Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
10 changes: 8 additions & 2 deletions .github/workflows/pull-request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,13 @@ jobs:

- name: Build spmgraph for Xcode 16.4
run: swift build
timeout-minutes: 4
timeout-minutes: 3

- name: Run tests on Xcode 16.4
env:
IS_RUNNING_TESTS: 1
run: swift test
timeout-minutes: 3

- name: Select Xcode 26.0 toolchain
uses: ./.github/actions/xcode-version
Expand All @@ -40,4 +46,4 @@ jobs:

- name: Build spmgraph for Xcode 26.0
run: swift build
timeout-minutes: 4
timeout-minutes: 3
10 changes: 8 additions & 2 deletions .swiftpm/xcode/xcshareddata/xcschemes/spmgraph.xcscheme
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,19 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
shouldUseLaunchSchemeArgsEnv = "YES">
<TestPlans>
<TestPlanReference
reference = "container:spmgraph.xctestplan"
default = "YES">
</TestPlanReference>
</TestPlans>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
enableAddressSanitizer = "YES"
enableASanStackUseAfterReturn = "YES"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
Expand Down
9 changes: 9 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -137,5 +137,14 @@ let package = Package(
),
]
),

// MARK: - Tests

.testTarget(
name: "SPMGraphExecutableTests",
dependencies: [
.target(name: "SPMGraphExecutable"),
]
)
]
)
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# spmgraph - SwiftPM dependency graph management

[![CI status](https://github.com/getyourguide/spmgraph/actions/workflows/pull-request.yml/badge.svg)](https://github.com/getyourguide/spmgraph/actions/workflows/pull-request.yml)
[![Swift Package Manager](https://rawgit.com/jlyonsmith/artwork/master/SwiftPackageManager/swiftpackagemanager-compatible.svg)](https://swift.org/package-manager/)

A CLI tool that **unlocks Swift dependency graphs**, giving you extra information and capabilities.
<br>
With it, you can visualize your dependency graph, run selective testing, and enforce architectural rules for optimal modular setups.
Expand Down Expand Up @@ -146,12 +149,11 @@ mint install getyourguide/spmgraph
## Acknowledgments
- Inspired by the work that the [Tuist](https://tuist.dev/) team does for the Apple developers community and their focus on leveraging the dependency graph to provide amazing features for engineers. Also, a source of inspiration for our shell abstraction layer.

## Open roadmap
## Open roadmap
- [ ] Cover the core logic of Lint, Map, and Visualize libs with tests
- [ ] Improve the `unusedDependencies` lint rule to cover products with multiple targets
- [ ] Support macros (to become a GitHub issue)
- [ ] Create Danger plugin for the linter functionality

Ideas
- [ ] Lint - see if it can be improved to cover auto-exported dependencies. For example, usages of `import Dependencies` justify linking `DependenciesExtras` as a dependency.
- [ ] Add fix-it suggestion to lint errors
- [ ] Create Danger plugin for the linter functionality
25 changes: 25 additions & 0 deletions Sources/Core/Extensions/ProcessInfo+Core.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
//
//
// Copyright (c) 2025 GetYourGuide GmbH
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
//

import Foundation

public extension ProcessInfo {
static var isRunningTests: Bool {
processInfo.environment["IS_RUNNING_TESTS"] != nil
}
}
2 changes: 1 addition & 1 deletion Sources/SPMGraphConfigSetup/Resources/Package.txt
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ let package = Package(
// TODO: Change it to HTTP when made public
.package(
url: "git@github.com:getyourguide/spmgraph.git",
branch: "main"
revision: "bda3a77facf780f921bab267628ce1c36afaadb1"
),

// TODO: Review which tag / Swift release to use
Expand Down
5 changes: 5 additions & 0 deletions Sources/SPMGraphConfigSetup/SPMGraphEdit.swift
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,11 @@ public final class SPMGraphEdit: SPMGraphEditProtocol {

private extension SPMGraphEdit {
func openEditPackage() throws(SPMGraphEditError) {
guard !ProcessInfo.isRunningTests else {
print("Skipped opening the edit package in tests...")
return
}

if verbose {
print("Opening the edit package...")
}
Expand Down
8 changes: 4 additions & 4 deletions Sources/SPMGraphConfigSetup/SPMGraphLoad.swift
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ public final class SPMGraphLoad: SPMGraphLoadProtocol {
editPackageDirectory
.appending(component: "Sources")
.appending(component: "SPMGraphConfig")
.appending(component: "DoNotEdit_DynamicLoading")
.appending(component: "_DoNotEdit_DynamicLoading")
.appending(extension: "swift")

public init(input: SPMGraphLoadInput) throws(SPMGraphLoadError) {
Expand All @@ -90,6 +90,8 @@ private extension SPMGraphLoad {
func load(userConfigFile: AbsolutePath) throws(SPMGraphLoadError) {
print("Loading your SPMGraphConfig.swift into spmgraph... please await")

defer { try? removeDynamicLoadingFile() }

try includeDynamicLoadingFile()

do {
Expand All @@ -115,16 +117,14 @@ private extension SPMGraphLoad {
)
}

defer { try? removeDynamicLoadingFile() }

print("Finished loading")
}

func includeDynamicLoadingFile() throws(SPMGraphLoadError) {
do {
guard
let dynamicLoadingTemplateURL = Bundle.module.url(
forResource: "Resources/DoNotEdit_DynamicLoading",
forResource: "Resources/_DoNotEdit_DynamicLoading",
withExtension: "txt"
)
else {
Expand Down
2 changes: 1 addition & 1 deletion Sources/SPMGraphExecutable/Subcommands/Tests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ struct TestsArguments: ParsableArguments {

@Option(
name: [.customLong("files"), .customLong("changedFiles")],
help: "Optional list of changed files. Otherwise git versioning is used"
help: "Optional list of changed files. Otherwise git versioning is used. It supports both absolute and relative paths"
)
var changedFiles: [String] = [] // TODO: Change to AbsolutePath

Expand Down
2 changes: 1 addition & 1 deletion Sources/SPMGraphExecutable/Subcommands/Visualize.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ struct VisualizeArguments: ParsableArguments {
@Option(
name: [.customShort("o"), .customLong("output", withSingleDash: false)],
help:
"Custom output file path for the generated PNG file. Default will generate a 'graph.png' file in the current directory"
"Custom output file path for the generated PNG file. By default a 'graph.png' file is generated in the current directory"
)
var outputFilePath: String?

Expand Down
7 changes: 5 additions & 2 deletions Sources/SPMGraphTests/SPMGraphTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -137,8 +137,11 @@ public final class SPMGraphTests: SSPMGraphTestsProtocol {
} else {
let changedFilesString = input.changedFiles
changedFiles = try changedFilesString.map {
let relativePath = try RelativePath(validating: $0)
return AbsolutePath.currentDir.appending(relativePath)
// Uses the AbsolutePath initializer that accepts either a relative or an absolute path
try AbsolutePath(
validating: $0,
relativeTo: .currentDir
)
}
}

Expand Down
50 changes: 27 additions & 23 deletions Sources/SPMGraphVisualize/SPMGraphVisualize.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public struct SPMGraphVisualizeInput {
/// Flag to exclude third-party dependencies from the graph declared in the `Package.swift`
let excludeThirdPartyDependencies: Bool
/// Custom output file path for the generated PNG file. Default will generate a 'graph.png' file in the current directory
let outputFilePath: String?
let outputFilePath: AbsolutePath
/// Minimum vertical spacing between the ranks (levels) of the graph. Default is set to 4. Is a double value in inches.
let rankSpacing: Double
/// Show extra logging for troubleshooting purposes
Expand All @@ -54,9 +54,20 @@ public struct SPMGraphVisualizeInput {
self.excludedSuffixes = excludedSuffixes
self.focusedModule = focusedModule
self.excludeThirdPartyDependencies = excludeThirdPartyDependencies
self.outputFilePath = outputFilePath
self.rankSpacing = rankSpacing
self.verbose = verbose

if let outputFilePath {
self.outputFilePath = try AbsolutePath(
validating: outputFilePath,
relativeTo: .currentDir
)
} else {
// the default output path
self.outputFilePath = AbsolutePath.currentDir
.appending(component: "graph")
.appending(extension: "png")
}
}
}

Expand Down Expand Up @@ -123,32 +134,25 @@ private extension SPMGraphVisualize {
graph.render(using: .dot, to: .png) { [weak system] result in
switch result {
case let .success(data):
let fileURL = URL(
fileURLWithPath: input.outputFilePath
?? FileManager.default.currentDirectoryPath.appending("/graph.png")
)

do {
try data.write(to: fileURL)

try system?
.run(
"open",
fileURL.absoluteString,
verbose: true
)
try data.write(to: input.outputFilePath.asURL)

// Opens the generated graph unless running tests
if !ProcessInfo.isRunningTests {
try system?
.run(
"open",
input.outputFilePath.pathString,
verbose: true
)
}
continuation.resume(returning: ())
} catch {
try? system?
.echo(
"Failed save and open graph visualization file with error: \(error.localizedDescription)"
)
continuation.resume(throwing: error)
}
case let .failure(error):
try? system?
.echo("Failed to render dependency graph with error: \(error.localizedDescription)")
continuation.resume(throwing: error)
}

continuation.resume(returning: ())
}
}
}
Expand Down
40 changes: 40 additions & 0 deletions Tests/Fixtures/Package/Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// swift-tools-version: 6.0

import PackageDescription

let package = Package(
name: "FixturePackage",
products: [
.library(
name: "Library",
targets: [
"TargetA",
"TargetB",
]
),
],
targets: [
.target(
name: "TargetA",
dependencies: [
"TargetB"
]
),
.target(
name: "TargetB"
),

.testTarget(
name: "TargetATests",
dependencies: [
"TargetA"
]
),
.testTarget(
name: "TargetBTests",
dependencies: [
"TargetB"
]
),
]
)
8 changes: 8 additions & 0 deletions Tests/Fixtures/Package/Sources/TargetA/Example.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import TargetB

struct Example {
static func main() {
print(Foo())
print(Bar())
}
}
11 changes: 11 additions & 0 deletions Tests/Fixtures/Package/Sources/TargetB/Example.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package struct Foo {
package let a = "dummy"

package init() {}
}

package struct Bar {
package let b = "baz"

package init() {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import TargetA
import Testing

@Suite
struct FooTests {
@Test bar() {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import TargetB
import Testing

@Suite
struct FooTests {
@Test bar() {}
}
Loading