Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
- Fix the wrong toolchain being shown as selected when using swiftly v1.0.1 ([#2014](https://github.com/swiftlang/vscode-swift/pull/2014))
- Fix extension displaying SwiftPM's project view and automatic build tasks even when `disableSwiftPMIntegration` was true ([#2011](https://github.com/swiftlang/vscode-swift/pull/2011))
- Validate extension settings and warn if they are invalid ([#2016](https://github.com/swiftlang/vscode-swift/pull/2016))
- Show progress when describing/listing dependencies on package load ([#2028](https://github.com/swiftlang/vscode-swift/pull/2028))

## 2.14.3 - 2025-12-15

Expand Down
35 changes: 19 additions & 16 deletions src/FolderContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ export class FolderContext implements vscode.Disposable {
/** dispose of any thing FolderContext holds */
dispose() {
this.linuxMain?.dispose();
this.swiftPackage.dispose();
this.packageWatcher.dispose();
this.testExplorer?.dispose();
this.backgroundCompilation.dispose();
Expand Down Expand Up @@ -138,11 +139,7 @@ export class FolderContext implements vscode.Disposable {
const { linuxMain, swiftPackage } =
await workspaceContext.statusItem.showStatusWhileRunning(statusItemText, async () => {
const linuxMain = await LinuxMain.create(folder);
const swiftPackage = await SwiftPackage.create(
folder,
toolchain,
configuration.disableSwiftPMIntegration
);
const swiftPackage = await SwiftPackage.create(folder);
return { linuxMain, swiftPackage };
});
workspaceContext.statusItem.end(statusItemText);
Expand All @@ -156,16 +153,22 @@ export class FolderContext implements vscode.Disposable {
workspaceContext
);

const error = await swiftPackage.error;
if (error) {
void vscode.window.showErrorMessage(
`Failed to load ${folderContext.name}/Package.swift: ${error.message}`
);
workspaceContext.logger.info(
`Failed to load Package.swift: ${error.message}`,
folderContext.name
);
}
// List the package's dependencies without blocking folder creation
void swiftPackage
.loadPackageState(folderContext)
.then(async () => await swiftPackage.error)
.catch(error => error)
.then(async error => {
if (error) {
void vscode.window.showErrorMessage(
`Failed to load ${folderContext.name}/Package.swift: ${error.message}`
);
workspaceContext.logger.info(
`Failed to load Package.swift: ${error.message}`,
folderContext.name
);
}
});

// Start watching for changes to Package.swift, Package.resolved and .swift-version
await folderContext.packageWatcher.install();
Expand Down Expand Up @@ -200,7 +203,7 @@ export class FolderContext implements vscode.Disposable {

/** reload swift package for this folder */
async reload() {
await this.swiftPackage.reload(this.toolchain, configuration.disableSwiftPMIntegration);
await this.swiftPackage.reload(this);
}

/** reload Package.resolved for this folder */
Expand Down
102 changes: 59 additions & 43 deletions src/SwiftPackage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,18 @@ import * as fs from "fs/promises";
import * as path from "path";
import * as vscode from "vscode";

import { FolderContext } from "./FolderContext";
import { describePackage } from "./commands/dependencies/describe";
import { showPackageDependencies } from "./commands/dependencies/show";
import { SwiftLogger } from "./logging/SwiftLogger";
import { BuildFlags } from "./toolchain/BuildFlags";
import { SwiftToolchain } from "./toolchain/toolchain";
import { isPathInsidePath } from "./utilities/filesystem";
import { lineBreakRegex } from "./utilities/tasks";
import { execSwift, getErrorDescription, hashString } from "./utilities/utilities";
import { execSwift, getErrorDescription, hashString, unwrapPromise } from "./utilities/utilities";

/** Swift Package Manager contents */
interface PackageContents {
export interface PackageContents {
name: string;
products: Product[];
dependencies: Dependency[];
Expand Down Expand Up @@ -196,9 +199,12 @@ function isError(state: SwiftPackageState): state is Error {
/**
* Class holding Swift Package Manager Package
*/
export class SwiftPackage {
export class SwiftPackage implements vscode.Disposable {
public plugins: PackagePlugin[] = [];
private _contents: SwiftPackageState | undefined;
private contentsPromise: Promise<SwiftPackageState>;
private contentsResolve: (value: SwiftPackageState | PromiseLike<SwiftPackageState>) => void;
private tokenSource: vscode.CancellationTokenSource = new vscode.CancellationTokenSource();

/**
* SwiftPackage Constructor
Expand All @@ -208,34 +214,26 @@ export class SwiftPackage {
*/
private constructor(
readonly folder: vscode.Uri,
private contentsPromise: Promise<SwiftPackageState>,
public resolved: PackageResolved | undefined,
// TODO: Make private again
public workspaceState: WorkspaceState | undefined
) {}
) {
const { promise, resolve } = unwrapPromise<SwiftPackageState>();
this.contentsPromise = promise;
this.contentsResolve = resolve;
}

/**
* Create a SwiftPackage from a folder
* @param folder folder package is in
* @param toolchain Swift toolchain to use
* @param disableSwiftPMIntegration Whether to disable SwiftPM integration
* @returns new SwiftPackage
*/
public static async create(
folder: vscode.Uri,
toolchain: SwiftToolchain,
disableSwiftPMIntegration: boolean = false
): Promise<SwiftPackage> {
public static async create(folder: vscode.Uri): Promise<SwiftPackage> {
const [resolved, workspaceState] = await Promise.all([
SwiftPackage.loadPackageResolved(folder),
SwiftPackage.loadWorkspaceState(folder),
]);
return new SwiftPackage(
folder,
SwiftPackage.loadPackage(folder, toolchain, disableSwiftPMIntegration),
resolved,
workspaceState
);
return new SwiftPackage(folder, resolved, workspaceState);
}

/**
Expand All @@ -259,45 +257,54 @@ export class SwiftPackage {
/**
* Run `swift package describe` and return results
* @param folder folder package is in
* @param toolchain Swift toolchain to use
* @param disableSwiftPMIntegration Whether to disable SwiftPM integration
* @returns results of `swift package describe`
*/
static async loadPackage(
folder: vscode.Uri,
toolchain: SwiftToolchain,
public async loadPackageState(
folderContext: FolderContext,
disableSwiftPMIntegration: boolean = false
): Promise<SwiftPackageState> {
const resolve = this.contentsResolve;
const result = await this.performLoadPackageState(folderContext, disableSwiftPMIntegration);
resolve(result);
return result;
}

private async performLoadPackageState(
folderContext: FolderContext,
disableSwiftPMIntegration: boolean = false
): Promise<SwiftPackageState> {
// When SwiftPM integration is disabled, return undefined to disable all features
if (disableSwiftPMIntegration) {
return undefined;
}

// If there is an existing package load, cancel any running taks first before loading a new one.
this.tokenSource.cancel();
this.tokenSource.dispose();
this.tokenSource = new vscode.CancellationTokenSource();

try {
// Use swift package describe to describe the package targets, products, and platforms
// Use swift package show-dependencies to get the dependencies in a tree format
const [describe, dependencies] = await Promise.all([
execSwift(["package", "describe", "--type", "json"], toolchain, {
cwd: folder.fsPath,
}),
execSwift(["package", "show-dependencies", "--format", "json"], toolchain, {
cwd: folder.fsPath,
}),
]);
const describe = await describePackage(folderContext, this.tokenSource.token);
const dependencies = await showPackageDependencies(
folderContext,
this.tokenSource.token
);

const packageState = {
...(JSON.parse(SwiftPackage.trimStdout(describe.stdout)) as PackageContents),
dependencies: JSON.parse(SwiftPackage.trimStdout(dependencies.stdout)).dependencies,
...(describe as PackageContents),
dependencies: dependencies,
};

return packageState;
} catch (error) {
const execError = error as { stderr: string };
} catch (error: unknown) {
const errorMessage = error instanceof Error ? error.message : String(error);
// if caught error and it begins with "error: root manifest" then there is no Package.swift
if (
execError.stderr !== undefined &&
(execError.stderr.startsWith("error: root manifest") ||
execError.stderr.startsWith("error: Could not find Package.swift"))
errorMessage.startsWith("error: root manifest") ||
errorMessage.startsWith("error: Could not find Package.swift")
) {
return undefined;
} else {
Expand Down Expand Up @@ -378,14 +385,18 @@ export class SwiftPackage {
}

/** Reload swift package */
public async reload(toolchain: SwiftToolchain, disableSwiftPMIntegration: boolean = false) {
const loadedContents = await SwiftPackage.loadPackage(
this.folder,
toolchain,
public async reload(folderContext: FolderContext, disableSwiftPMIntegration: boolean = false) {
const { promise, resolve } = unwrapPromise<SwiftPackageState>();
this.contentsPromise = promise;
this.contentsResolve = resolve;

const loadedContents = await this.performLoadPackageState(
folderContext,
disableSwiftPMIntegration
);

this._contents = loadedContents;
this.contentsPromise = Promise.resolve(loadedContents);
resolve(loadedContents);
}

/** Reload Package.resolved file */
Expand Down Expand Up @@ -573,14 +584,19 @@ export class SwiftPackage {
);
}

private static trimStdout(stdout: string): string {
static trimStdout(stdout: string): string {
// remove lines from `swift package describe` until we find a "{"
while (!stdout.startsWith("{")) {
const firstNewLine = stdout.indexOf("\n");
stdout = stdout.slice(firstNewLine + 1);
}
return stdout;
}

dispose() {
this.tokenSource.cancel();
this.tokenSource.dispose();
}
}

export enum TargetType {
Expand Down
125 changes: 125 additions & 0 deletions src/commands/dependencies/describe.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the VS Code Swift open source project
//
// Copyright (c) 2021-2025 the VS Code Swift project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of VS Code Swift project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//
import * as vscode from "vscode";

import { FolderContext } from "../../FolderContext";
import { PackageContents, SwiftPackage } from "../../SwiftPackage";
import { SwiftTaskProvider, createSwiftTask } from "../../tasks/SwiftTaskProvider";
import { packageName } from "../../utilities/tasks";
import { executeTaskWithUI, updateAfterError } from "../utilities";

/**
* Configuration for executing a Swift package command
*/
export interface SwiftPackageCommandConfig {
/** The Swift command arguments (e.g., ["package", "show-dependencies", "--format", "json"]) */
args: string[];
/** The task name for the SwiftTaskProvider */
taskName: string;
/** The UI message to display during execution */
uiMessage: string;
/** The command name for error messages */
commandName: string;
}

/**
* Execute a Swift package command and return the parsed JSON output
* @param folderContext folder to run the command in
* @param config command configuration
* @returns parsed JSON output from the command
*/
export async function executeSwiftPackageCommand<T>(
folderContext: FolderContext,
config: SwiftPackageCommandConfig,
token?: vscode.CancellationToken
): Promise<T> {
const task = createSwiftTask(
config.args,
config.taskName,
{
cwd: folderContext.folder,
scope: folderContext.workspaceFolder,
packageName: packageName(folderContext),
presentationOptions: { reveal: vscode.TaskRevealKind.Silent },
dontTriggerTestDiscovery: true,
group: vscode.TaskGroup.Build,
},
folderContext.toolchain,
undefined,
{ readOnlyTerminal: true }
);

const outputChunks: string[] = [];
task.execution.onDidWrite((data: string) => {
outputChunks.push(data);
});

const success = await executeTaskWithUI(
task,
config.uiMessage,
folderContext,
false,
false,
token
);
updateAfterError(success, folderContext);

const output = outputChunks.join("");

if (!success) {
throw new Error(output);
}

if (!output.trim()) {
throw new Error(`No output received from swift ${config.commandName} command`);
}

try {
const trimmedOutput = SwiftPackage.trimStdout(output);
const parsedOutput = JSON.parse(trimmedOutput);

// Validate the parsed output is an object
if (!parsedOutput || typeof parsedOutput !== "object") {
throw new Error(`Invalid format received from swift ${config.commandName} command`);
}

return parsedOutput as T;
} catch (parseError) {
throw new Error(
`Failed to parse ${config.commandName} output: ${parseError instanceof Error ? parseError.message : "Unknown error"}`
);
}
}

/**
* Run `swift package describe` inside a folder
* @param folderContext folder to run describe for
*/
export async function describePackage(
folderContext: FolderContext,
token?: vscode.CancellationToken
): Promise<PackageContents> {
const result = await executeSwiftPackageCommand<PackageContents>(
folderContext,
{
args: ["package", "describe", "--type", "json"],
taskName: SwiftTaskProvider.describePackageName,
uiMessage: "Describing Package",
commandName: "package describe",
},
token
);

return result;
}
Loading