Skip to content

Commit a106e4b

Browse files
davdresbmxavNinjaLikesCheezBoy Baukema
authored
Release/1.0.0 (#98)
* Handle targets with same name but in different projects * Ignore test bundles when generating bitcode * Issue #81 log unrequired targets instead of failing. * Issue #81 PIF workspace sort ENG-55520 * On sourceTree decode error, log & continue * SourceTree: refactor to class * SENG-9588 remove development release * SSAST-10500 Warn if multiple builds in xcode log (#91) * SSAST-10519 A --capture option has been added to collect diagnostic data into a debug-data sub-folder of the IR directory. What data has been captured is written to the Gen-IR log. * Install instructions cannot be executed as is, improve install instructions to make them executable * Feature/ssast 10888 (#96) * SSAST-10888 don't copy dependent modules but save them for pp to reconstruct. --------- Co-authored-by: bmxav <[email protected]> Co-authored-by: NinjaLikesCheez <[email protected]> Co-authored-by: Boy Baukema <[email protected]>
1 parent 0bfd3f7 commit a106e4b

36 files changed

Lines changed: 2353 additions & 20 deletions

File tree

README.md

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,18 @@ To **install and run** the tool, you'll need Homebrew, Xcode, and macOS 12 or gr
2626
## Install
2727

2828
```bash
29-
# If you don't have brew installed, install it: https://brew.sh/
30-
31-
# Add the brew tap to your local machine
32-
brew tap veracode/tap
33-
34-
# Install the tool
35-
brew install gen-ir
29+
if ! command -v brew >/dev/null 2>&1; then
30+
echo "Homebrew is not installed. Visit https://brew.sh/ to install it."
31+
return 1 2>/dev/null || true
32+
else
33+
echo "Installing gen-ir using Homebrew" &&
34+
brew tap veracode/tap &&
35+
brew install gen-ir &&
36+
37+
echo "gen-ir installed successfully."
38+
echo "For usage instructions, see:"
39+
echo " https://github.com/veracode/gen-ir#readme"
40+
fi
3641
```
3742

3843
## Update (if previously installed)

Sources/DependencyGraph/DependencyGraph.swift

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,18 +37,31 @@ public class DependencyGraph<Value: NodeValue> {
3737
/// - Parameter value: the associated value for a node to start the search with
3838
/// - Returns: the chain of nodes, starting with the 'bottom' of the dependency subgraph
3939
public func chain(for value: Value) -> [Node] {
40+
let noFilter: Set<String> = []
41+
return chainWithFilter(for: value, filter: noFilter)
42+
}
43+
44+
/// Returns the dependency 'chain' for the value associated with a node in the graph using a depth-first search
45+
/// while filtering dynamic dependencies.
46+
/// - Parameter value: the associated value for a node to start the search with
47+
/// - Parameter value: a set whose keys indicate node values which will not be chased further.
48+
/// - Returns: the chain of nodes, starting with the 'bottom' of the dependency subgraph
49+
public func chainWithFilter(for value: Value, filter dynamicDependencyFilter: Set<String>) -> [Node] {
4050
guard let node = findNode(for: value) else {
4151
GenIRLogger.logger.debug("Couldn't find node for value: \(value.valueName)")
4252
return []
4353
}
4454

45-
return depthFirstSearch(startingAt: node)
55+
return depthFirstSearchWithFilter(startingAt: node, filter: dynamicDependencyFilter)
4656
}
4757

4858
/// Perform a depth-first search starting at the provided node
4959
/// - Parameter node: the node whose children to search through
60+
/// - Parameter filter: A set of String. If a dependency relationship in the graph is contained in
61+
/// the Set, then add that edge to the chain and continue with the next edge without descending
62+
/// further down the graph.
5063
/// - Returns: an array of nodes ordered by a depth-first search approach
51-
private func depthFirstSearch(startingAt node: Node) -> [Node] {
64+
private func depthFirstSearchWithFilter(startingAt node: Node, filter dynamicDependencyFilter: Set<String>) -> [Node] {
5265
GenIRLogger.logger.debug("----\nSearching for: \(node.value.valueName)")
5366
var visited = Set<Node>()
5467
var chain = [Node]()
@@ -60,6 +73,11 @@ public class DependencyGraph<Value: NodeValue> {
6073
visited.insert(node)
6174

6275
for edge in node.edges where edge.relationship == .dependency {
76+
if dynamicDependencyFilter.contains(edge.to.valueName) {
77+
GenIRLogger.logger.debug("\tskipping dependency: \(edge.to.valueName)")
78+
chain.append(edge.to)
79+
continue
80+
}
6381
if visited.insert(edge.to).inserted {
6482
GenIRLogger.logger.debug("edge to: \(edge.to)")
6583
depthFirst(node: edge.to)

Sources/GenIR/CompilerCommandRunner.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ struct CompilerCommandRunner {
6565
continue
6666
}
6767

68-
GenIRLogger.logger.info("Operating on target: \(target.name). Total modules processed: \(totalModulesRun)")
68+
GenIRLogger.logger.debug("Operating on target: \(target.name). Total modules processed: \(totalModulesRun)")
6969

7070
totalModulesRun += try run(commands: targetCommands, for: target.productName, at: output)
7171
}
@@ -90,7 +90,7 @@ struct CompilerCommandRunner {
9090
var targetModulesRun = 0
9191

9292
for (index, command) in commands.enumerated() {
93-
GenIRLogger.logger.info(
93+
GenIRLogger.logger.debug(
9494
"""
9595
\(dryRun ? "Dry run of" : "Running") command (\(command.compiler.rawValue)) \(index + 1) of \(commands.count). \
9696
Target modules processed: \(targetModulesRun)

Sources/GenIR/GenIR.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,6 @@ struct DebuggingOptions: ParsableArguments {
9797
// to capture the log output to a file.
9898
if debuggingOptions.capture {
9999
debugData = try DebugData(xcodeArchivePath: xcarchivePath)
100-
101100
}
102101

103102
// Initialize the logger

Sources/GenIR/OutputPostprocessor.swift

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ class OutputPostprocessor {
5656
try manager.createDirectory(at: output, withIntermediateDirectories: false)
5757

5858
for node in nodes {
59+
var targetDependencies: [String: [String]] = [:]
5960
let dependers = node.edges.filter { $0.relationship == .depender }
6061

6162
guard dynamicDependencyToPath[node.value.productName] != nil || (dependers.count == 0 && !node.value.isSwiftPackage) else {
@@ -78,22 +79,33 @@ class OutputPostprocessor {
7879

7980
// Copy over this target's static dependencies
8081
var processed: Set<Target> = []
81-
try copyDependencies(for: node.value, to: irDirectory, processed: &processed)
82+
try copyDependencies(for: node.value, to: irDirectory, processed: &processed, savedDependencies: &targetDependencies)
83+
84+
// Persist the dependency map for this target
85+
try persistDynamicDependencies(map: targetDependencies, to: irDirectory.appendingPathComponent("savedDependencies.json"))
8286
}
8387
}
8488

85-
private func copyDependencies(for target: Target, to irDirectory: URL, processed: inout Set<Target>) throws {
89+
private func copyDependencies(for target: Target, to irDirectory: URL, processed: inout Set<Target>, savedDependencies: inout [String: [String]]) throws {
8690
guard processed.insert(target).inserted else {
8791
return
8892
}
8993

90-
for node in graph.chain(for: target) {
91-
GenIRLogger.logger.debug("Processing Node: \(node.valueName)")
94+
for node in graph.chainWithFilter(for: target, filter: Set(dynamicDependencyToPath.keys)) {
95+
GenIRLogger.logger.debug("Processing Node with product: \(node.value.productName) and value: \(node.valueName)")
9296

9397
// Do not copy dynamic dependencies
94-
guard dynamicDependencyToPath[node.value.productName] == nil else { continue }
98+
guard dynamicDependencyToPath[node.value.productName] == nil else {
99+
// Skip this directory for any dynamic dependency that is not the current one being processed. During preprocessing on the
100+
// platform the modules for this dependency will be retrieved and added to this module.
101+
if irDirectory.lastPathComponent != node.value.productName {
102+
savedDependencies[irDirectory.lastPathComponent, default: []].append(node.value.productName)
103+
}
104+
GenIRLogger.logger.debug(" ---> Skipping dynamic dependency: \(node.value.productName)")
105+
continue
106+
}
95107

96-
try copyDependencies(for: node.value, to: irDirectory, processed: &processed)
108+
try copyDependencies(for: node.value, to: irDirectory, processed: &processed, savedDependencies: &savedDependencies)
97109

98110
let buildDirectory = build.appendingPathComponent(node.value.productName)
99111
if manager.directoryExists(at: buildDirectory) {
@@ -185,4 +197,19 @@ class OutputPostprocessor {
185197
GenIRLogger.logger.debug("Couldn't determine the base search path for the xcarchive, using: \(productsPath)")
186198
return productsPath
187199
}
200+
201+
/// Persist the dynamic dependecies in the IR folder. The preprocessor will use this data to build modules
202+
/// for BCA.
203+
private func persistDynamicDependencies(map dynamicDependencyToPath: [String: [String]], to destination: URL) throws {
204+
// Convert URL to string for serialization
205+
let serializableDict = dynamicDependencyToPath.mapValues { $0 }
206+
207+
// JSON encode
208+
let encoder = JSONEncoder()
209+
encoder.outputFormatting = [.sortedKeys, .prettyPrinted]
210+
let data = try encoder.encode(serializableDict)
211+
212+
// Write to file
213+
try data.write(to: URL(fileURLWithPath: destination.filePath))
214+
}
188215
}

Sources/GenIR/Versions.swift

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,14 @@
55
// Created by Thomas Hedderwick on 12/09/2022.
66
//
77
// History:
8-
// 2025-nn-nn - 0.5.4 -- Update release doc; warn multiple builds; capture debug data
8+
// 2025-nn-nn - 1.0.0 -- Don't chase through Dynamic Dependencies
9+
// 2025-09-19 - 0.5.4 -- Update release doc; warn multiple builds; capture debug data
910
// 2025-04-18 - 0.5.3 -- PIF Tracing; log unique compiler commands
1011
// 2025-04-09 - 0.5.2 -- PIF sort workspace; log instead of throw
1112
// 2024-09-17 - 0.5.1
1213
// 2024-09-16 - 0.5.0 -- Process based on the PIF cache instead of project files.
1314
import Foundation
1415

1516
enum Versions {
16-
static let version = "0.5.4"
17+
static let version = "1.0.0"
1718
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
Test project in TestApp77.xcworkspace (built originally with Xcode 16.1).
2+
3+
4+
TestApp77 (app target) depends on TestLibraryA (framework target) and TestLibraryC (framework target)
5+
6+
TestLibraryA (framework target) depends on TestLibraryB (framework target)
7+
8+
TestLibraryB (framework target) depends on the opentelemetry-swift package.
9+
10+
TestLibraryC (framework target) has no dependencies.
11+
12+
13+
14+
Generated archive is in build/test.xcarchive.
15+
16+
17+
---
18+
See that the IR files from the opentelemetry-swift package (e.g., ZipkinBaggagePropagator.bc) appear in all targets,
19+
except for TestLibraryC (which doesn't have a transitive dependency on opentelemetry-swift).
20+
21+
However, the files should only appear in IR/TestLibraryB, which is the target that references the package. You can
22+
check that only Products/Applications/TestApp77.app/Frameworks/TestLibraryB.framework/TestLibraryB contains symbols
23+
from opentelemetry-swift (e.g., using the `nm` utility).
24+
25+
Why is this a problem? The size of the IR folder is bloating the app:
26+
27+
$ du -chd 1 build/test.xcarchive/IR
28+
26M build/test.xcarchive/IR/TestApp77.app
29+
26M build/test.xcarchive/IR/TestLibraryB.framework
30+
4.0K build/test.xcarchive/IR/TestLibraryC.framework
31+
26M build/test.xcarchive/IR/TestLibraryA.framework
32+
78M build/test.xcarchive/IR
33+
78M total
34+
35+
36+
This app has no actual contents: only TestLibraryB should have anything of
37+
significance from the opentelemetry-swift package (26M), and everything else
38+
should be ~4K. However, we're getting the 26M from opentelemetry-swift multiplied
39+
across all transitive dependencies, which for a big app with ~200 frameworks, can
40+
be multiple gigabytes of extra space.
41+
42+
In particular, we're seeing our zipped app archive go above the 2GiB upload limit!
43+
44+
45+
---
46+
Build command:
47+
48+
rm -rf ./build && xcodebuild clean -workspace TestApp77.xcworkspace -scheme TestApp77 -derivedDataPath ./build && xcodebuild archive -derivedDataPath ./build -workspace TestApp77.xcworkspace -scheme TestApp77 -configuration Debug -destination generic/platform=iOS -archivePath build/test.xcarchive > build_log.txt
49+
50+
51+
Gen-IR command:
52+
53+
gen-ir build_log.txt build/test.xcarchive --debug > gen_ir_log.txt

TestAssets/DependencyChaseFilter/TestApp77.xcworkspace/contents.xcworkspacedata

Lines changed: 20 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+
<plist version="1.0">
4+
<dict/>
5+
</plist>

0 commit comments

Comments
 (0)