Skip to content

feat(ios): improve spm build logs, clarity and size reporting#6085

Open
NathanWalker wants to merge 1 commit into
mainfrom
feat/spm-log-improvements
Open

feat(ios): improve spm build logs, clarity and size reporting#6085
NathanWalker wants to merge 1 commit into
mainfrom
feat/spm-log-improvements

Conversation

@NathanWalker

@NathanWalker NathanWalker commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Improvements to SPM resolutions in the build logs.

Current behavior?

When a project uses Swift Packages, the CLI gives little insight into what is
happening during -resolvePackageDependencies:

  • There is no upfront listing of which packages will be installed or where
    they resolve from, so a slow or failing resolve can't be attributed to a
    specific dependency.
  • A package hosted in a repository with a large git history can "fetch" for
    many minutes with zero output (SwiftPM clones the entire history to read
    Package.swift), which looks like the CLI is hung.
  • When resolution fails, the spinner swallows xcodebuild's output — the real
    error is only visible by rerunning with --log trace.
  • A local SPM package declared by a plugin with a plugin-relative path fails
    to resolve in non-hoisted node_modules layouts, and two plugins declaring
    the same package produce duplicate pbxproj references.

New behavior

Upfront package listing. Before applying packages, the CLI prints every
package and its source:

Swift Packages:
FontManager (1.0.12 · https://github.com/NativeScript/font-manager.git)
CanvasNative (local: node_modules/@nativescript/canvas/platforms/ios)

Visible progress during long fetches. While a package is fetching, the
CLI measures its growing clone in the SwiftPM repository cache and shows real
progress in the spinner instead of a frozen counter:

⠇ Fetching Swift Packages (Auth0.swift)… — 2.31 MB of git history fetched (5s)

If a single clone passes 250 MB, a one-time note explains that SwiftPM clones
the entire repository on first fetch (and where the cache lives), so users
know the wait is expected.

Actionable failures. The resolve step keeps a rolling tail of raw
xcodebuild output (also logged at trace) and replays the last 25 lines when
resolution fails, so the actual error is visible without a verbose rerun.
Local packages whose path doesn't exist are now warned about immediately
instead of surfacing later as a cryptic xcodebuild failure.

Plugin package fixes (ios-project-service):

  • A plugin-declared local package path that doesn't exist relative to the app
    project dir now falls back to resolving against the plugin's own directory
    (fixes non-hoisted node_modules layouts).
  • When multiple plugins declare the same package, only the first is added —
    duplicates no longer create conflicting pbxproj references.
  • Plugin native source groups are now pinned to the main app target, so they
    can no longer be compiled into an extension target (e.g. a widget) that
    happens to own the first "Sources" build phase.

Summary by CodeRabbit

  • Bug Fixes

    • Improved resolution of plugin-provided local Swift package paths.
    • Prevented native source files from being associated with the wrong Xcode target.
    • Deduplicated overlapping Swift package declarations.
  • Improvements

    • Added clearer Swift package listings and local-path validation.
    • Enhanced dependency resolution progress, clone-size reporting, timing, and failure logs.
    • Added more informative success and error messages during Swift package resolution.

@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

SwiftPM package merging now removes duplicate plugin declarations, reports package and resolver activity, monitors clone growth, and surfaces failure output. iOS project integration also resolves plugin-relative package paths and pins native sources to the first Xcode target.

Changes

SwiftPM and iOS integration

Layer / File(s) Summary
Package preparation and listing
lib/services/ios/spm-service.ts, test/spm-service.ts
Plugin packages are deduplicated, package sources are listed and validated, formatting helpers are added, and listing output is tested.
SwiftPM resolution progress and diagnostics
lib/services/ios/spm-service.ts
Progress mode tracks cache clone growth, buffers resolver output, and reports elapsed-time success or failure details.
Native project dependency and target integration
lib/services/ios-project-service.ts
Plugin-relative package paths are normalized, and native source groups are assigned to the first Xcode target.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

Suggested reviewers: farfromrefug

Poem

I’m a rabbit with packages tucked in my tray,
Deduping the bundles that hop on their way.
Clone sizes grow, and the spinner shines bright,
Resolver tails help when things don’t go right.
Sources find targets, paths settle down—
SwiftPM hops neatly through project town.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: improved iOS SwiftPM logging, progress visibility, and package size reporting.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
lib/services/ios/spm-service.ts (2)

232-255: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Synchronous cache-directory walk runs inside the progress ticker.

getPackageCloneSizeBytes/getDirectorySizeBytes do a fully synchronous recursive readdirSync/statSync walk, invoked every 5s from setInterval while precisely the scenario this code targets (a multi-GB git clone) is in progress. This blocks the event loop — including processing of onProgress chunks from the xcodebuild child process — for however long the walk takes, which grows with the very clone size being measured.

♻️ Suggested mitigation
-		const ticker = setInterval(() => {
-			tickCount++;
-			if (fetchingPackageRef && tickCount % 5 === 0) {
-				fetchedBytes = this.getPackageCloneSizeBytes(fetchingPackageRef);
+		let measuring = false;
+		const ticker = setInterval(() => {
+			tickCount++;
+			if (fetchingPackageRef && tickCount % 5 === 0 && !measuring) {
+				measuring = true;
+				setImmediate(() => {
+					try {
+						fetchedBytes = this.getPackageCloneSizeBytes(fetchingPackageRef);
+					} finally {
+						measuring = false;
+					}
+				});

Converting the walk itself to fs.promises would be a more thorough fix but requires more restructuring; guarding against overlap and yielding via setImmediate/async fs are lower-effort mitigations.

Also applies to: 448-501

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/services/ios/spm-service.ts` around lines 232 - 255, Prevent the progress
ticker from running the synchronous getPackageCloneSizeBytes directory walk on
the event loop during an active clone. Update the setInterval callback and
related size-check flow to avoid overlapping measurements and yield or defer the
cache-size work so onProgress processing remains responsive, while preserving
the existing five-second polling and one-time large-clone notification behavior.

9-31: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use the injected $fs for SwiftPM cache size checks.

getPackageCloneSizeBytes and getDirectorySizeBytes call raw fs.readdirSync/fs.statSync, while this service uses this.$fs.exists, readText, etc. For the same reason, the size-reporting flow has no coverage for these helpers in test/spm-service.ts and should include those cases when wrapping the fs operation.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/services/ios/spm-service.ts` around lines 9 - 31, The SwiftPM clone-size
helpers bypass the injected filesystem dependency and therefore cannot be
covered by the service tests. Update getPackageCloneSizeBytes and
getDirectorySizeBytes to use this.$fs for readdir/stat operations, preserving
their existing size-reporting behavior, and add test coverage in the
corresponding size-reporting flow for the wrapped filesystem cases.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@lib/services/ios-project-service.ts`:
- Around line 1532-1540: Update the target selection in the project.addPbxGroup
call to resolve and use the main application target’s UUID explicitly, rather
than relying on project.getFirstTarget().uuid. Ensure the pinned Sources group
always targets the app target and never an extension or watch target.

---

Nitpick comments:
In `@lib/services/ios/spm-service.ts`:
- Around line 232-255: Prevent the progress ticker from running the synchronous
getPackageCloneSizeBytes directory walk on the event loop during an active
clone. Update the setInterval callback and related size-check flow to avoid
overlapping measurements and yield or defer the cache-size work so onProgress
processing remains responsive, while preserving the existing five-second polling
and one-time large-clone notification behavior.
- Around line 9-31: The SwiftPM clone-size helpers bypass the injected
filesystem dependency and therefore cannot be covered by the service tests.
Update getPackageCloneSizeBytes and getDirectorySizeBytes to use this.$fs for
readdir/stat operations, preserving their existing size-reporting behavior, and
add test coverage in the corresponding size-reporting flow for the wrapped
filesystem cases.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 189feab7-04d2-4990-9d80-9b37304abda2

📥 Commits

Reviewing files that changed from the base of the PR and between f5154ac and 5a0ca1d.

📒 Files selected for processing (3)
  • lib/services/ios-project-service.ts
  • lib/services/ios/spm-service.ts
  • test/spm-service.ts

Comment on lines +1532 to +1540
// pin the sources to the main app target: without an explicit target the
// underlying xcode lib picks whichever "Sources" build phase it finds
// first, which can be an extension target (e.g. a widget) once one
// exists — compiling plugin native code into extensions breaks their
// builds (and bloats them) since they lack the app's search paths.
project.addPbxGroup(group.files, group.name, group.path, null, {
isMain: true,
filesRelativeToProject: true,
target: project.getFirstTarget()?.uuid,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n 'getFirstTarget' -A 10 -g '*.js' -g '*.ts' node_modules/@nativescript/nativescript-dev-xcode 2>/dev/null
rg -n 'getFirstTarget' lib/definitions/nativescript-dev-xcode.d.ts

Repository: NativeScript/nativescript-cli

Length of output: 208


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate and inspect the implementation and relevant usage context without running repository code.
echo "== find package files =="
git ls-files | rg '(^|/)node_modules/@nativescript/nativescript-dev-xcode/|^packages/.*/lib/services/ios-project-service\.ts$|lib/services/ios-project-service\.ts$' || true

echo
echo "== getFirstTarget references =="
rg -n 'getFirstTarget|addExtensions|prepareProject|addPbxGroup' -S \
  -g '*.js' -g '*.ts' \
  . \
  | head -n 200

echo
echo "== package declaration files mentioning nativescript-dev-xcode =="
for f in package.json pnpm-lock.yaml yarn.lock package-lock.json; do
  if [ -f "$f" ]; then
    echo "--- $f ---"
    rg -n '`@nativescript/nativescript-dev-xcode`|nativescript-dev-xcode' "$f" | head -n 50 || true
  fi
done

echo
echo "== current working tree status =="
git status --short | head -n 50 || true

Repository: NativeScript/nativescript-cli

Length of output: 2972


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the relevant implementation if the package is present in the checkout.
if [ -d node_modules/@nativescript/nativescript-dev-xcode ]; then
  echo "== nativescript-dev-xcode package contents =="
  git ls-files node_modules/@nativescript/nativescript-dev-xcode | head -n 200 || true
  echo
  echo "== getFirstTarget implementation candidates =="
  rg -n 'getFirstTarget|getTargets|targets\s*=|targetNames|firstTarget' node_modules/@nativescript/nativescript-dev-xcode -S -A 12 -B 8
else
  echo "node_modules/@nativescript/nativescript-dev-xcode not present"
fi

Repository: NativeScript/nativescript-cli

Length of output: 229


🌐 Web query:

@nativescript/nativescript-dev-xcode 0.8.2 getFirstTarget source

💡 Result:

The package @nativescript/nativescript-dev-xcode is a fork of apache/cordova-node-xcode [1][2]. Searches performed across the repository and related documentation did not locate a function named getFirstTarget within the @nativescript/nativescript-dev-xcode codebase [1][3][4]. While the term getFirstTarget appears in other unrelated projects (such as Java-based implementations or other libraries) [5][6][7], it is not a documented or standard part of the @nativescript/nativescript-dev-xcode API [2][3]. The primary API for this library involves methods like addHeaderFile, addSourceFile, and addFramework, which are used after parsing a project file [2][3].

Citations:


🌐 Web query:

site:nativescript.org or site:github.com/NativeScript/nativescript dev-xcode getFirstTarget

💡 Result:

The term dev-xcode refers to nativescript-dev-xcode, which is a Node.js library used to parse and modify Xcode project (.xcodeproj) files [1]. It was forked from the cordova-node-xcode project [1]. There is no standard, widely documented function named getFirstTarget within the public API of the nativescript-dev-xcode library [1]. While the library allows for the manipulation of Xcode project files, interaction with targets typically involves accessing the project's internal data structures, such as the pbxproj object, which represents the project's configuration [1]. If you are looking to interact with targets in an Xcode project using this library, you would typically parse the project file first and then access the targets array within the project object [1]: const xcode = require("xcode"); const projectPath = "myproject.xcodeproj/project.pbxproj"; const myProj = xcode.project(projectPath); myProj.parse(function (err) { // Targets are usually stored in myProj.hash.project.objects.PBXNativeTarget const targets = myProj.pbxNativeTargets; const firstTarget = targets; console.log(firstTarget); }); Because nativescript-dev-xcode is a wrapper around cordova-node-xcode, its API methods are consistent with the original library, which primarily exposes methods like addHeaderFile, addSourceFile, and addFramework [1]. You may need to inspect the specific version of the library you are using to confirm if any custom helper methods exist, or access the targets directly via the parsed object [1].

Citations:


🌐 Web query:

npm package nativescript-dev-xcode 0.8.2 getFirstTarget

💡 Result:

In the nativescript-dev-xcode package (which is a fork of cordova-node-xcode), the getFirstTarget method is a utility function used to retrieve the first target defined within an Xcode project (.pbxproj) file [1][2][3]. Based on its implementation and standard usage in projects derived from this parser, getFirstTarget returns an object containing the UUID and the target details [3][4][5]. Typical usage often looks like this: const target = xcode.getFirstTarget; // target.uuid provides the UUID of the first target // target.firstTarget provides the object representation of the target The method is frequently used in build scripts and plugin logic (such as Cordova or NativeScript plugins) to automatically identify the main project target when performing operations like adding build phases, modifying build settings, or embedding frameworks [6]. While version 0.8.2 is the latest release (published June 10, 2026) [7][8], the getFirstTarget method is a long-standing feature in this codebase, inherited from the original cordova-node-xcode parser [1].

Citations:


🌐 Web query:

github.com NativeScript nativescript-dev-xcode getFirstTarget implementation

💡 Result:

In the nativescript-dev-xcode library, which is a fork of cordova-node-xcode [1][2], the getFirstTarget method is intended to retrieve the first build target defined within the Xcode project file [3][4][5]. Based on the library's implementation history and shared ancestry with node-xcode, the method generally accesses the project's native targets section and returns the first entry found [3][4]. It typically returns an object containing the UUID of the target and the target definition itself (often labeled as pbxNativeTarget) [4][5]. Usage example: const target = xcode.getFirstTarget; [3] Technical details: 1. Origin: The library is a parser utility for.xcodeproj/project.pbxproj files [1][2]. 2. Implementation Context: The method is used in automated build processes to identify the primary application or library target, though it can sometimes lead to issues if multiple targets (such as iOS and tvOS) are present, as it indiscriminately selects the first one [6]. 3. Signatures: You can find type definitions for this method in various related projects that consume the same node-xcode-style API, where it is defined as returning an object containing the UUID and the target details [7][4][5]. For specific implementation code, you should examine the lib/pbxProject.js file within the NativeScript/nativescript-dev-xcode repository on GitHub, as this file contains the core project parsing logic and method definitions [1][8][9].

Citations:


Use the main-target UUID explicitly before pinning Sources.

getFirstTarget() only exposes a uuid shape here; its implementation is not available locally to prove it excludes extension/watch targets. If an extension/watch target is pinned instead of the main app target, the Sources phase still resolves to the wrong target.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/services/ios-project-service.ts` around lines 1532 - 1540, Update the
target selection in the project.addPbxGroup call to resolve and use the main
application target’s UUID explicitly, rather than relying on
project.getFirstTarget().uuid. Ensure the pinned Sources group always targets
the app target and never an extension or watch target.

@NathanWalker NathanWalker added this to the 9.1 milestone Jul 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant