Build Automation
Automating builds using the ProjectAutomation framework.
Overview
The ProjectAutomation framework allows you to programmatically access project structure and automate build processes. This is useful for creating custom CI/CD pipelines, build scripts, and development tools.
Loading the Project Graph
The foundation of build automation is loading the project graph:
import ProjectAutomation
// Load the graph at the current directory
let graph = try Tuist.graph()
// Or load at a specific path
let graph = try Tuist.graph(at: "/path/to/project")
Tuist.graph() Method
The path at which the graph should be loaded. If nil, the current directory is used.
Returns the loaded graph containing all projects, targets, and schemes.
Graph Structure
The returned Graph object contains:
The name of the workspace or project.
The absolute path to the workspace or project.
An array of all projects in the graph.
Project Structure
Each Project in the graph has:
The absolute path of the project.
Indicates whether the project is imported through Package.swift.
The Swift packages that this project depends on.
The targets this project produces.
The schemes available to this project.
Target Structure
Each Target contains:
The product type the target produces (e.g., “app”, “framework”, “unit_tests”).
The bundle identifier of the target.
List of file paths that are the target’s sources.
List of file paths that are the target’s resources.
The target’s build settings.
The target’s dependencies.
Example: Building All Apps
import Foundation
import ProjectAutomation
@main
struct BuildAllApps {
static func main() throws {
let graph = try Tuist.graph()
print("Loading project graph from: \(graph.path)")
print("Workspace: \(graph.name)\n")
// Find all app targets
var appTargets: [(project: String, target: Target)] = []
for project in graph.projects {
for target in project.targets where target.product == "app" {
appTargets.append((project.name, target))
}
}
print("Found \(appTargets.count) app targets:\n")
// Build each app
for (projectName, target) in appTargets {
print("Building \(target.name) in \(projectName)...")
try buildApp(target: target.name, scheme: target.name)
print("✓ \(target.name) built successfully\n")
}
}
static func buildApp(target: String, scheme: String) throws {
let process = Process()
process.executableURL = URL(fileURLWithPath: "/usr/bin/xcodebuild")
process.arguments = [
"build",
"-scheme", scheme,
"-destination", "generic/platform=iOS",
"-configuration", "Release"
]
try process.run()
process.waitUntilExit()
guard process.terminationStatus == 0 else {
throw BuildError.buildFailed(scheme: scheme)
}
}
enum BuildError: Error {
case buildFailed(scheme: String)
}
}
Example: Conditional Building
Build only specific targets based on criteria:
import Foundation
import ProjectAutomation
@main
struct ConditionalBuild {
static func main() throws {
let graph = try Tuist.graph()
let targetsToBuild = ProcessInfo.processInfo.environment["TARGETS"]?.split(separator: ",").map(String.init) ?? []
for project in graph.projects {
for target in project.targets {
// Build if specified in environment or if it's an app
let shouldBuild = targetsToBuild.contains(target.name) || target.product == "app"
if shouldBuild {
print("Building \(target.name) (\(target.product))...")
try buildTarget(target, in: project)
}
}
}
}
static func buildTarget(_ target: Target, in project: Project) throws {
// Build implementation
print(" Bundle ID: \(target.bundleId)")
print(" Sources: \(target.sources.count) files")
print(" Dependencies: \(target.dependencies.count)")
}
}
Example: Build Order Analysis
Analyze and determine build order based on dependencies:
import Foundation
import ProjectAutomation
@main
struct BuildOrderAnalyzer {
static func main() throws {
let graph = try Tuist.graph()
// Collect all targets
var allTargets: [String: Target] = [:]
for project in graph.projects {
for target in project.targets {
allTargets[target.name] = target
}
}
// Analyze dependencies
print("Build Order Analysis:\n")
for (name, target) in allTargets.sorted(by: { $0.key < $1.key }) {
print("\(name):")
print(" Product: \(target.product)")
print(" Dependencies: \(target.dependencies.count)")
// List direct dependencies
for dependency in target.dependencies {
switch dependency {
case .target(let depName):
print(" -> \(depName)")
case .external(let depName):
print(" -> [external] \(depName)")
default:
print(" -> [other]")
}
}
print()
}
}
}
Example: CI/CD Integration
Integrate with CI/CD systems:
import Foundation
import ProjectAutomation
@main
struct CIBuild {
static func main() throws {
let graph = try Tuist.graph()
let configuration = ProcessInfo.processInfo.environment["CONFIGURATION"] ?? "Debug"
let platform = ProcessInfo.processInfo.environment["PLATFORM"] ?? "iOS"
print("CI Build Configuration:")
print(" Configuration: \(configuration)")
print(" Platform: \(platform)\n")
// Build all non-test targets
for project in graph.projects {
for target in project.targets {
guard !target.product.contains("test") else { continue }
print("Building \(target.name)...")
// Extract build settings
let settings = target.settings
print(" Base settings: \(settings.base.count) keys")
print(" Configurations: \(settings.configurations.count)")
// Perform build
try executeBuild(
target: target.name,
configuration: configuration,
platform: platform
)
}
}
}
static func executeBuild(target: String, configuration: String, platform: String) throws {
// Execute xcodebuild or tuist build
let process = Process()
process.executableURL = URL(fileURLWithPath: "/usr/bin/env")
process.arguments = [
"tuist", "build",
target,
"--configuration", configuration,
"--platform", platform
]
try process.run()
process.waitUntilExit()
if process.terminationStatus == 0 {
print(" ✓ Success\n")
} else {
throw BuildError.failed(target: target)
}
}
enum BuildError: Error {
case failed(target: String)
}
}
Best Practices
-
Error Handling: Always handle potential errors when loading the graph or building targets.
-
Parallel Builds: Consider building independent targets in parallel for faster execution.
-
Configuration: Use environment variables for configuration options (build configuration, platform, etc.).
-
Logging: Provide clear logging for build progress and results.
-
Validation: Validate the graph structure before attempting builds.