How putnami observe reads an iOS or Swift repository
Two detectors read a Swift repository: the Swift Package Manager detector
reads each Package.swift, and the Xcode detector each .xcodeproj. Neither
runs swift, xcodebuild or pod. The change analyses use what they find
to name the changed and dependent components, place tests and coverage, and
diff Package.resolved and Podfile.lock.
Swift packages
A directory that holds Package.swift is a Swift package. The package is a
component, named by its package name, else by its directory, and unnamed at
the repository root when the manifest does not spell its name as a literal, and so is each of its targets, at the
target's directory and named by the target. Both are of ecosystem swift and
source workspace. The package depends on each of its targets other than its
test targets, so a change to a target marks its package dependent, and a
change to Package.swift itself marks the package changed.
The export
Package.swift is Swift code: only Swift can evaluate it. When the CI writes
the package description beside the manifest, before putnami observe, it is
the authority:
swift package describe --type json > swiftpm.jsonRun it in each package directory. Each target of the export is a component at
its path. Each name in a target's target_dependencies is an edge to that
target of the same package. Each name in its product_dependencies is an edge
to the local package that declares the product: a .package(path:) of the
manifest, or a fileSystem dependency of the export. The export names a local
package by its absolute path on the machine that wrote it, so it is matched by
its identity, the lowercased last segment of the path, against the manifest's
paths, else by the trailing segments of the path. A product of a remote
package is no edge. Keep the export out of the commit, as a file the job
writes.
A swiftpm.json that does not parse, or lists no target, gets
observe.swiftpm_export_unreadable, and the package is read without it.
Without the export
The static read takes each directory under Sources as a target, and each
directory under Tests as a test target. Each .package(path:) of
Package.swift, relative to the package directory, is a local package: each
target under Sources depends on it, and each target under Tests depends
on every target under Sources. The name: argument of Package( names the
package; comments are left out.
The static read cannot see a target's own dependencies, a custom target
path: or a condition, so it errs towards more edges. One info diagnostic,
observe.swiftpm_static_read, names the packages read so and the export
command.
Xcode projects
Each .xcodeproj is a component at the directory that holds it, named by the
project, of ecosystem xcode and source manifest. Its targets are not
components of their own: the project's directory holds the app's sources and
its test targets' directories. Each XCLocalSwiftPackageReference of
project.pbxproj is an edge to the package at its relativePath, relative to
the project's directory, so a change to a local package marks the app
dependent. XCLocalSwiftPackageReference is how Xcode 15 and later record a
local package; an older project that adds a package as a folder reference
gets no edge. Remote package references are no edge: Package.resolved holds
their versions. Several .xcodeproj in one directory are one component,
named by the first in name order, with the edges and test targets of all of
them.
A project.pbxproj that does not parse gets observe.xcode_project_unreadable,
and the project stays a component with no edge. When a Swift package and an
Xcode project hold one directory, the package wins.
The walk
The detectors walk the repository. They skip hidden directories, such as
.build and .swiftpm, and Pods, Carthage, DerivedData,
SourcePackages and node_modules, and do not enter an .xcodeproj or an
.xcworkspace. A workspace is not read: the projects and packages it groups
are read on their own.
Reports
putnami observe reads no .xcresult bundle. Convert it in the CI:
| Report | Command |
|---|---|
| JUnit | xcodebuild test ... | xcbeautify --report junit, or fastlane scan |
| JUnit | swift test --xunit-output build/reports/xunit.xml for a package |
| Cobertura | xcresultparser --output-format cobertura --project-root . Test.xcresult > coverage.xml, or slather |
| LCOV | swift test --enable-code-coverage, then llvm-cov export -format=lcov |
These reporters name a test case's class Module.Class, such as
PricingTests.PriceCalculatorTests, and write no file. The case goes to the
component of the test target named Module: a test target of a Swift package,
or a unit or UI test target of an Xcode project. A target name with characters
Swift does not allow in a module name, such as -, matches with _ in their
place. A class name without a module is looked for in every test target. The
case's file is Class.swift under the target's directory, else the Swift file
there that declares the class. An Xcode test target's directory is the one
named by the target beside the project, else the project's directory. A
coverage report's files map as in any repository: see
the change analyses.
Lockfiles
A Package.resolved, wherever it lies, such as
App.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/ or a workspace's
xcshareddata/swiftpm/, is a lockfile of ecosystem swift: each pin by its
identity, at its version, else at its revision for a pin on a branch or a
commit. Format versions 1 to 3 are read; version 1 names a pin by its package.
A Podfile.lock is a lockfile of ecosystem cocoapods: each entry of PODS
by its root pod, so Firebase/Core and Firebase/Analytics are one package
Firebase, at the version in parentheses. A pod that EXTERNAL SOURCES
installs from a :path: is the repository's own code and is left out.
Limits
- Package traits, conditional dependencies and plugins are read only through the export.
- A CocoaPods pod, a Carthage framework and a remote Swift package are no component.
- An Xcode target's own dependencies on another target of its project are not read: the project is one component.