Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
0cdc1a6
Handle targets with same name but in different projects
bmxav Sep 17, 2024
dbe4b44
Merge pull request #78 from veracode/project-specific-targets
bmxav Sep 26, 2024
c088f08
Ignore test bundles when generating bitcode
bmxav Sep 26, 2024
874e98a
Update build.yml
davdres Apr 2, 2025
6e7a8a6
Update build.yml
davdres Apr 2, 2025
31602e8
Update build.yml
davdres Apr 2, 2025
a175ddc
Merge pull request #79 from veracode/ignore-test-bundles
bmxav Sep 26, 2024
09075b8
Update build.yml
davdres Apr 2, 2025
17aec35
Issue #81 log unrequired targets instead of failing.
davdres Mar 11, 2025
3341d77
Issue #81 log PIF location & missing commands.
davdres Mar 19, 2025
310c00d
Issue #81 PIF workspace sort
davdres Mar 27, 2025
e65d74c
Issue #81 resolve lint error.
davdres Mar 27, 2025
8ce1496
Issue #81 Update version; add some logging
davdres Apr 1, 2025
013c07f
Update PIF/Sources/PIFSupport/PIF.swift
davdres Apr 2, 2025
1508b5c
Update PIF/Sources/PIFSupport/PIFSupport.swift
davdres Apr 2, 2025
020fef6
Issue #81 Address linter & review issues.
davdres Apr 2, 2025
0b1812c
Issue #81 Test unwrap of optional pif cache location.
davdres Apr 2, 2025
dde9586
Issue 81 add running of tests to the build pipeline
davdres Apr 2, 2025
1bc6b8b
Merge pull request #83 from veracode/unknown-targets
davdres Apr 3, 2025
6f8f588
ENG-55520: On sourceTree decode error, log & continue (#88)
davdres Apr 17, 2025
c41046b
SENG-9588 remove development release (#90)
davdres Apr 23, 2025
fd8abb5
SSAST-10500 Warn if multiple builds in xcode log (#91)
davdres Apr 24, 2025
d7fb8ef
SSAST-10519 Add option to capture debug data (#92)
davdres Jun 2, 2025
51cb602
SSAST-10900 Update help for debug data capture. (#93)
davdres Jul 31, 2025
be30b57
Install instructions cannot be executed as is, improve install instru…
Nov 3, 2025
3edb28d
Merge pull request #97 from veracode/feature/improved-install-shell
NinjaLikesCheez Nov 3, 2025
a1dbe58
Feature/ssast 10888 (#96)
davdres Nov 17, 2025
1eb09b9
Merge branch 'main' into release/1.0.0
davdres Nov 21, 2025
50cb5f3
SSAST-10888 keep release branch consistent with develop.
davdres Nov 21, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 12 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,18 @@ To **install and run** the tool, you'll need Homebrew, Xcode, and macOS 12 or gr
## Install

```bash
# If you don't have brew installed, install it: https://brew.sh/

# Add the brew tap to your local machine
brew tap veracode/tap

# Install the tool
brew install gen-ir
if ! command -v brew >/dev/null 2>&1; then
echo "Homebrew is not installed. Visit https://brew.sh/ to install it."
return 1 2>/dev/null || true
else
echo "Installing gen-ir using Homebrew" &&
brew tap veracode/tap &&
brew install gen-ir &&

echo "gen-ir installed successfully."
echo "For usage instructions, see:"
echo " https://github.com/veracode/gen-ir#readme"
fi
```

## Update (if previously installed)
Expand Down
22 changes: 20 additions & 2 deletions Sources/DependencyGraph/DependencyGraph.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,18 +37,31 @@ public class DependencyGraph<Value: NodeValue> {
/// - Parameter value: the associated value for a node to start the search with
/// - Returns: the chain of nodes, starting with the 'bottom' of the dependency subgraph
public func chain(for value: Value) -> [Node] {
let noFilter: Set<String> = []
return chainWithFilter(for: value, filter: noFilter)
}

/// Returns the dependency 'chain' for the value associated with a node in the graph using a depth-first search
/// while filtering dynamic dependencies.
/// - Parameter value: the associated value for a node to start the search with
/// - Parameter value: a set whose keys indicate node values which will not be chased further.
/// - Returns: the chain of nodes, starting with the 'bottom' of the dependency subgraph
public func chainWithFilter(for value: Value, filter dynamicDependencyFilter: Set<String>) -> [Node] {
guard let node = findNode(for: value) else {
GenIRLogger.logger.debug("Couldn't find node for value: \(value.valueName)")
return []
}

return depthFirstSearch(startingAt: node)
return depthFirstSearchWithFilter(startingAt: node, filter: dynamicDependencyFilter)
}

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

for edge in node.edges where edge.relationship == .dependency {
if dynamicDependencyFilter.contains(edge.to.valueName) {
GenIRLogger.logger.debug("\tskipping dependency: \(edge.to.valueName)")
chain.append(edge.to)
continue
}
if visited.insert(edge.to).inserted {
GenIRLogger.logger.debug("edge to: \(edge.to)")
depthFirst(node: edge.to)
Expand Down
4 changes: 2 additions & 2 deletions Sources/GenIR/CompilerCommandRunner.swift
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ struct CompilerCommandRunner {
continue
}

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

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

for (index, command) in commands.enumerated() {
GenIRLogger.logger.info(
GenIRLogger.logger.debug(
"""
\(dryRun ? "Dry run of" : "Running") command (\(command.compiler.rawValue)) \(index + 1) of \(commands.count). \
Target modules processed: \(targetModulesRun)
Expand Down
1 change: 0 additions & 1 deletion Sources/GenIR/GenIR.swift
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,6 @@ struct DebuggingOptions: ParsableArguments {
// to capture the log output to a file.
if debuggingOptions.capture {
debugData = try DebugData(xcodeArchivePath: xcarchivePath)

}

// Initialize the logger
Expand Down
39 changes: 33 additions & 6 deletions Sources/GenIR/OutputPostprocessor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ class OutputPostprocessor {
try manager.createDirectory(at: output, withIntermediateDirectories: false)

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

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

// Copy over this target's static dependencies
var processed: Set<Target> = []
try copyDependencies(for: node.value, to: irDirectory, processed: &processed)
try copyDependencies(for: node.value, to: irDirectory, processed: &processed, savedDependencies: &targetDependencies)

// Persist the dependency map for this target
try persistDynamicDependencies(map: targetDependencies, to: irDirectory.appendingPathComponent("savedDependencies.json"))
}
}

private func copyDependencies(for target: Target, to irDirectory: URL, processed: inout Set<Target>) throws {
private func copyDependencies(for target: Target, to irDirectory: URL, processed: inout Set<Target>, savedDependencies: inout [String: [String]]) throws {
guard processed.insert(target).inserted else {
return
}

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

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

try copyDependencies(for: node.value, to: irDirectory, processed: &processed)
try copyDependencies(for: node.value, to: irDirectory, processed: &processed, savedDependencies: &savedDependencies)

let buildDirectory = build.appendingPathComponent(node.value.productName)
if manager.directoryExists(at: buildDirectory) {
Expand Down Expand Up @@ -185,4 +197,19 @@ class OutputPostprocessor {
GenIRLogger.logger.debug("Couldn't determine the base search path for the xcarchive, using: \(productsPath)")
return productsPath
}

/// Persist the dynamic dependecies in the IR folder. The preprocessor will use this data to build modules
/// for BCA.
private func persistDynamicDependencies(map dynamicDependencyToPath: [String: [String]], to destination: URL) throws {
// Convert URL to string for serialization
let serializableDict = dynamicDependencyToPath.mapValues { $0 }

// JSON encode
let encoder = JSONEncoder()
encoder.outputFormatting = [.sortedKeys, .prettyPrinted]
let data = try encoder.encode(serializableDict)

// Write to file
try data.write(to: URL(fileURLWithPath: destination.filePath))
}
}
5 changes: 3 additions & 2 deletions Sources/GenIR/Versions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@
// Created by Thomas Hedderwick on 12/09/2022.
//
// History:
// 2025-nn-nn - 0.5.4 -- Update release doc; warn multiple builds; capture debug data
// 2025-nn-nn - 1.0.0 -- Don't chase through Dynamic Dependencies
// 2025-09-19 - 0.5.4 -- Update release doc; warn multiple builds; capture debug data
// 2025-04-18 - 0.5.3 -- PIF Tracing; log unique compiler commands
// 2025-04-09 - 0.5.2 -- PIF sort workspace; log instead of throw
// 2024-09-17 - 0.5.1
// 2024-09-16 - 0.5.0 -- Process based on the PIF cache instead of project files.
import Foundation

enum Versions {
static let version = "0.5.4"
static let version = "1.0.0"
}
53 changes: 53 additions & 0 deletions TestAssets/DependencyChaseFilter/README.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
Test project in TestApp77.xcworkspace (built originally with Xcode 16.1).


TestApp77 (app target) depends on TestLibraryA (framework target) and TestLibraryC (framework target)

TestLibraryA (framework target) depends on TestLibraryB (framework target)

TestLibraryB (framework target) depends on the opentelemetry-swift package.

TestLibraryC (framework target) has no dependencies.



Generated archive is in build/test.xcarchive.


---
See that the IR files from the opentelemetry-swift package (e.g., ZipkinBaggagePropagator.bc) appear in all targets,
except for TestLibraryC (which doesn't have a transitive dependency on opentelemetry-swift).

However, the files should only appear in IR/TestLibraryB, which is the target that references the package. You can
check that only Products/Applications/TestApp77.app/Frameworks/TestLibraryB.framework/TestLibraryB contains symbols
from opentelemetry-swift (e.g., using the `nm` utility).

Why is this a problem? The size of the IR folder is bloating the app:

$ du -chd 1 build/test.xcarchive/IR
26M build/test.xcarchive/IR/TestApp77.app
26M build/test.xcarchive/IR/TestLibraryB.framework
4.0K build/test.xcarchive/IR/TestLibraryC.framework
26M build/test.xcarchive/IR/TestLibraryA.framework
78M build/test.xcarchive/IR
78M total


This app has no actual contents: only TestLibraryB should have anything of
significance from the opentelemetry-swift package (26M), and everything else
should be ~4K. However, we're getting the 26M from opentelemetry-swift multiplied
across all transitive dependencies, which for a big app with ~200 frameworks, can
be multiple gigabytes of extra space.

In particular, we're seeing our zipped app archive go above the 2GiB upload limit!


---
Build command:

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


Gen-IR command:

gen-ir build_log.txt build/test.xcarchive --debug > gen_ir_log.txt

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict/>
</plist>
Loading