diff --git a/Fixtures/Miscellaneous/ShowTraits/app/Package.swift b/Fixtures/Miscellaneous/ShowTraits/app/Package.swift new file mode 100644 index 00000000000..710890704c1 --- /dev/null +++ b/Fixtures/Miscellaneous/ShowTraits/app/Package.swift @@ -0,0 +1,26 @@ +// swift-tools-version:6.1 +import PackageDescription + +let package = Package( + name: "Dealer", + products: [ + .executable( + name: "dealer", + targets: ["Dealer"] + ), + ], + traits: [ + .trait(name: "trait1", description: "this trait is the default in app"), + .trait(name: "trait2", description: "this trait is not the default in app"), + .default(enabledTraits: ["trait1"]), + ], + dependencies: [ + .package(path: "../deck-of-playing-cards", traits: ["trait3"]), + ], + targets: [ + .executableTarget( + name: "Dealer", + path: "./" + ), + ] +) diff --git a/Fixtures/Miscellaneous/ShowTraits/app/main.swift b/Fixtures/Miscellaneous/ShowTraits/app/main.swift new file mode 100644 index 00000000000..83174e0afff --- /dev/null +++ b/Fixtures/Miscellaneous/ShowTraits/app/main.swift @@ -0,0 +1 @@ +print("I'm the dealer") diff --git a/Fixtures/Miscellaneous/ShowTraits/deck-of-playing-cards/Package.swift b/Fixtures/Miscellaneous/ShowTraits/deck-of-playing-cards/Package.swift new file mode 100644 index 00000000000..8bac348eeae --- /dev/null +++ b/Fixtures/Miscellaneous/ShowTraits/deck-of-playing-cards/Package.swift @@ -0,0 +1,21 @@ +// swift-tools-version:6.1 +import PackageDescription + +let package = Package( + name: "deck-of-playing-cards", + products: [ + .executable( + name: "deck", + targets: ["Deck"] + ), + ], + traits: [ + .trait(name: "trait3", description: "This trait is in a different package and not default.") + ], + targets: [ + .executableTarget( + name: "Deck", + path: "./" + ), + ] +) diff --git a/Fixtures/Miscellaneous/ShowTraits/deck-of-playing-cards/main.swift b/Fixtures/Miscellaneous/ShowTraits/deck-of-playing-cards/main.swift new file mode 100644 index 00000000000..51bd0e13de2 --- /dev/null +++ b/Fixtures/Miscellaneous/ShowTraits/deck-of-playing-cards/main.swift @@ -0,0 +1 @@ +print("I'm a deck of cards") diff --git a/Sources/Commands/CMakeLists.txt b/Sources/Commands/CMakeLists.txt index e29fdd0e091..303944aebcc 100644 --- a/Sources/Commands/CMakeLists.txt +++ b/Sources/Commands/CMakeLists.txt @@ -31,6 +31,7 @@ add_library(Commands PackageCommands/Resolve.swift PackageCommands/ShowDependencies.swift PackageCommands/ShowExecutables.swift + PackageCommands/ShowTraits.swift PackageCommands/SwiftPackageCommand.swift PackageCommands/ToolsVersionCommand.swift PackageCommands/Update.swift diff --git a/Sources/Commands/PackageCommands/ShowTraits.swift b/Sources/Commands/PackageCommands/ShowTraits.swift new file mode 100644 index 00000000000..128415ebff1 --- /dev/null +++ b/Sources/Commands/PackageCommands/ShowTraits.swift @@ -0,0 +1,87 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See http://swift.org/LICENSE.txt for license information +// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +import ArgumentParser +import Basics +import CoreCommands +import Foundation +import PackageModel +import PackageGraph +import Workspace + +struct ShowTraits: AsyncSwiftCommand { + static let configuration = CommandConfiguration( + abstract: "List the available traits for a package.") + + @OptionGroup(visibility: .hidden) + var globalOptions: GlobalOptions + + @Option(help: "Show traits for any package id in the transitive dependencies.") + var packageId: String? + + @Option(help: "Set the output format.") + var format: ShowTraitsMode = .text + + func run(_ swiftCommandState: SwiftCommandState) async throws { + let packageGraph = try await swiftCommandState.loadPackageGraph() + + let traits = if let packageId { + packageGraph.packages.filter({ $0.identity.description == packageId }).flatMap( { $0.manifest.traits } ).sorted(by: {$0.name < $1.name} ) + } else { + packageGraph.rootPackages.flatMap( { $0.manifest.traits } ).sorted(by: {$0.name < $1.name} ) + } + + switch self.format { + case .text: + let defaultTraits = traits.filter( { $0.isDefault } ).flatMap( { $0.enabledTraits }) + + for trait in traits { + guard !trait.isDefault else { + continue + } + + print("\(trait.name)\(trait.description ?? "" != "" ? " - " + trait.description! : "")\(defaultTraits.contains(trait.name) ? " (default)" : "")") + } + + case .json: + let encoder = JSONEncoder() + encoder.outputFormatting = .sortedKeys + + let data = try encoder.encode(traits) + if let output = String(data: data, encoding: .utf8) { + print(output) + } + } + } + + enum ShowTraitsMode: String, RawRepresentable, CustomStringConvertible, ExpressibleByArgument, CaseIterable { + case text, json + + public init?(rawValue: String) { + switch rawValue.lowercased() { + case "text": + self = .text + case "json": + self = .json + default: + return nil + } + } + + public var description: String { + switch self { + case .text: return "text" + case .json: return "json" + } + } + } +} diff --git a/Sources/Commands/PackageCommands/SwiftPackageCommand.swift b/Sources/Commands/PackageCommands/SwiftPackageCommand.swift index da253850404..473bb6e54ed 100644 --- a/Sources/Commands/PackageCommands/SwiftPackageCommand.swift +++ b/Sources/Commands/PackageCommands/SwiftPackageCommand.swift @@ -66,6 +66,7 @@ public struct SwiftPackageCommand: AsyncParsableCommand { ShowDependencies.self, ShowExecutables.self, + ShowTraits.self, ToolsVersionCommand.self, ComputeChecksum.self, ArchiveSource.self, diff --git a/Sources/PackageManagerDocs/Documentation.docc/Package/PackageShowTraits.md b/Sources/PackageManagerDocs/Documentation.docc/Package/PackageShowTraits.md new file mode 100644 index 00000000000..c00467909e9 --- /dev/null +++ b/Sources/PackageManagerDocs/Documentation.docc/Package/PackageShowTraits.md @@ -0,0 +1,303 @@ +# swift package show-traits + +@Metadata { + @PageImage(purpose: icon, source: command-icon) +} + +List the available traits for a package. + +``` +package show-traits [--package-path=] + [--cache-path=] [--config-path=] + [--security-path=] + [--scratch-path=] + [--swift-sdks-path=] + [--toolset=...] + [--pkg-config-path=...] + [--enable-dependency-cache] [--disable-dependency-cache] + [--enable-build-manifest-caching] + [--disable-build-manifest-caching] + [--manifest-cache=] + [--enable-experimental-prebuilts] + [--disable-experimental-prebuilts] [--verbose] + [--very-verbose|vv] [--quiet] [--color-diagnostics] + [--no-color-diagnostics] [--disable-sandbox] [--netrc] + [--enable-netrc] [--disable-netrc] + [--netrc-file=] [--enable-keychain] + [--disable-keychain] + [--resolver-fingerprint-checking=] + [--resolver-signing-entity-checking=] + [--enable-signature-validation] + [--disable-signature-validation] [--enable-prefetching] + [--disable-prefetching] + [--force-resolved-versions|disable-automatic-resolution|only-use-versions-from-resolved-file] + [--skip-update] [--disable-scm-to-registry-transformation] + [--use-registry-identity-for-scm] + [--replace-scm-with-registry] + [--default-registry-url=] + [--configuration=] [--=...] + [--=...] [--=...] [--=...] + [--triple=] [--sdk=] [--toolchain=] + [--swift-sdk=] [--sanitize=...] + [--auto-index-store] [--enable-index-store] + [--disable-index-store] + [--enable-parseable-module-interfaces] [--jobs=] + [--use-integrated-swift-driver] + [--explicit-target-dependency-import-check=] + [--build-system=] [--=] + [--enable-dead-strip] [--disable-dead-strip] + [--disable-local-rpath] [--format=] [--package-id=] [--version] + [--help] +``` + +- term **--package-path=\**: + +*Specify the package path to operate on (default current directory). This changes the working directory before any other operation.* + + +- term **--cache-path=\**: + +*Specify the shared cache directory path.* + + +- term **--config-path=\**: + +*Specify the shared configuration directory path.* + + +- term **--security-path=\**: + +*Specify the shared security directory path.* + + +- term **--scratch-path=\**: + +*Specify a custom scratch directory path. (default .build)* + + +- term **--swift-sdks-path=\**: + +*Path to the directory containing installed Swift SDKs.* + + +- term **--toolset=\**: + +*Specify a toolset JSON file to use when building for the target platform. Use the option multiple times to specify more than one toolset. Toolsets will be merged in the order they're specified into a single final toolset for the current build.* + + +- term **--pkg-config-path=\**: + +*Specify alternative path to search for pkg-config `.pc` files. Use the option multiple times to +specify more than one path.* + + +- term **--enable-dependency-cache|disable-dependency-cache**: + +*Use a shared cache when fetching dependencies.* + + +- term **--enable-build-manifest-caching|disable-build-manifest-caching**: + + +- term **--manifest-cache=\**: + +*Caching mode of Package.swift manifests. Valid values are: (shared: shared cache, local: package's build directory, none: disabled)* + + +- term **--enable-experimental-prebuilts|disable-experimental-prebuilts**: + +*Whether to use prebuilt swift-syntax libraries for macros.* + + +- term **--verbose**: + +*Increase verbosity to include informational output.* + + +- term **--very-verbose|vv**: + +*Increase verbosity to include debug output.* + + +- term **--quiet**: + +*Decrease verbosity to only include error output.* + + +- term **--color-diagnostics|no-color-diagnostics**: + +*Enables or disables color diagnostics when printing to a TTY. +By default, color diagnostics are enabled when connected to a TTY and disabled otherwise.* + + +- term **--disable-sandbox**: + +*Disable using the sandbox when executing subprocesses.* + + +- term **--netrc**: + +*Use netrc file even in cases where other credential stores are preferred.* + + +- term **--enable-netrc|disable-netrc**: + +*Load credentials from a netrc file.* + + +- term **--netrc-file=\**: + +*Specify the netrc file path.* + + +- term **--enable-keychain|disable-keychain**: + +*Search credentials in macOS keychain.* + + +- term **--resolver-fingerprint-checking=\**: + + +- term **--resolver-signing-entity-checking=\**: + + +- term **--enable-signature-validation|disable-signature-validation**: + +*Validate signature of a signed package release downloaded from registry.* + + +- term **--enable-prefetching|disable-prefetching**: + + +- term **--force-resolved-versions|disable-automatic-resolution|only-use-versions-from-resolved-file**: + +*Only use versions from the Package.resolved file and fail resolution if it is out-of-date.* + + +- term **--skip-update**: + +*Skip updating dependencies from their remote during a resolution.* + + +- term **--disable-scm-to-registry-transformation**: + +*Disable source control to registry transformation.* + + +- term **--use-registry-identity-for-scm**: + +*Look up source control dependencies in the registry and use their registry identity when possible to help deduplicate across the two origins.* + + +- term **--replace-scm-with-registry**: + +*Look up source control dependencies in the registry and use the registry to retrieve them instead of source control when possible.* + + +- term **--default-registry-url=\**: + +*Default registry URL to use, instead of the registries.json configuration file.* + + +- term **--configuration=\**: + +*Build with configuration* + + +- term **--=\**: + +*Pass flag through to all C compiler invocations.* + + +- term **--=\**: + +*Pass flag through to all Swift compiler invocations.* + + +- term **--=\**: + +*Pass flag through to all linker invocations.* + + +- term **--=\**: + +*Pass flag through to all C++ compiler invocations.* + + +- term **--triple=\**: + + +- term **--sdk=\**: + + +- term **--toolchain=\**: + + +- term **--swift-sdk=\**: + +*Filter for selecting a specific Swift SDK to build with.* + + +- term **--sanitize=\**: + +*Turn on runtime checks for erroneous behavior, possible values: address, thread, undefined, scudo.* + + +- term **--auto-index-store|enable-index-store|disable-index-store**: + +*Enable or disable indexing-while-building feature.* + + +- term **--enable-parseable-module-interfaces**: + + +- term **--jobs=\**: + +*The number of jobs to spawn in parallel during the build process.* + + +- term **--use-integrated-swift-driver**: + + +- term **--explicit-target-dependency-import-check=\**: + +*A flag that indicates this build should check whether targets only import their explicitly-declared dependencies.* + + +- term **--build-system=\**: + + +- term **--=\**: + +*The Debug Information Format to use.* + + +- term **--enable-dead-strip|disable-dead-strip**: + +*Disable/enable dead code stripping by the linker.* + + +- term **--disable-local-rpath**: + +*Disable adding $ORIGIN/@loader_path to the rpath by default.* + + +- term **--format=\**: + +*Set the output format. Available formats: flatlist (default), json* + + +- term **--package-id=\**: + +*Select a package to display the traits. Default is the current root package.* + + +- term **--version**: + +*Show the version.* + + +- term **--help**: + +*Show help information.* + diff --git a/Sources/PackageManagerDocs/Documentation.docc/SwiftPackageCommands.md b/Sources/PackageManagerDocs/Documentation.docc/SwiftPackageCommands.md index 17bd12fcb64..af599efa723 100644 --- a/Sources/PackageManagerDocs/Documentation.docc/SwiftPackageCommands.md +++ b/Sources/PackageManagerDocs/Documentation.docc/SwiftPackageCommands.md @@ -41,6 +41,7 @@ Overview of package manager commands here... - - - +- - - - diff --git a/Sources/_InternalTestSupport/SwiftTesting+Tags.swift b/Sources/_InternalTestSupport/SwiftTesting+Tags.swift index f8af94b5f30..92fe1554098 100644 --- a/Sources/_InternalTestSupport/SwiftTesting+Tags.swift +++ b/Sources/_InternalTestSupport/SwiftTesting+Tags.swift @@ -71,6 +71,7 @@ extension Tag.Feature.Command.Package { @Tag public static var Resolve: Tag @Tag public static var ShowDependencies: Tag @Tag public static var ShowExecutables: Tag + @Tag public static var ShowTraits: Tag @Tag public static var ToolsVersion: Tag @Tag public static var Unedit: Tag @Tag public static var Update: Tag diff --git a/Tests/CommandsTests/PackageCommandTests.swift b/Tests/CommandsTests/PackageCommandTests.swift index 39fe3e0135f..3c985140638 100644 --- a/Tests/CommandsTests/PackageCommandTests.swift +++ b/Tests/CommandsTests/PackageCommandTests.swift @@ -1196,6 +1196,115 @@ struct PackageCommandTests { } } + @Test( + .tags( + .Feature.Command.Package.ShowTraits, + ), + arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms), + ) + func showTraits( + data: BuildData, + ) async throws { + try await fixture(name: "Miscellaneous/ShowTraits") { fixturePath in + let packageRoot = fixturePath.appending("app") + var (textOutput, _) = try await execute( + ["show-traits", "--format=text"], + packagePath: packageRoot, + configuration: data.config, + buildSystem: data.buildSystem, + ) + #expect(textOutput.contains("trait1 - this trait is the default in app (default)")) + #expect(textOutput.contains("trait2 - this trait is not the default in app")) + #expect(!textOutput.contains("trait3")) + + var (jsonOutput, _) = try await execute( + ["show-traits", "--format=json"], + packagePath: packageRoot, + configuration: data.config, + buildSystem: data.buildSystem, + ) + var json = try JSON(bytes: ByteString(encodingAsUTF8: jsonOutput)) + guard case .array(let contents) = json else { + Issue.record("unexpected result") + return + } + + #expect(3 == contents.count) + + guard case let first = contents.first else { + Issue.record("unexpected result") + return + } + guard case .dictionary(let `default`) = first else { + Issue.record("unexpected result") + return + } + #expect(`default`["name"]?.stringValue == "default") + guard case .array(let enabledTraits) = `default`["enabledTraits"] else { + Issue.record("unexpected result") + return + } + #expect(enabledTraits.count == 1) + let firstEnabledTrait = enabledTraits[0] + #expect(firstEnabledTrait.stringValue == "trait1") + + guard case let second = contents[1] else { + Issue.record("unexpected result") + return + } + guard case .dictionary(let trait1) = second else { + Issue.record("unexpected result") + return + } + #expect(trait1["name"]?.stringValue == "trait1") + + guard case let third = contents[2] else { + Issue.record("unexpected result") + return + } + guard case .dictionary(let trait2) = third else { + Issue.record("unexpected result") + return + } + #expect(trait2["name"]?.stringValue == "trait2") + + // Show traits for the dependency based on its package id + (textOutput, _) = try await execute( + ["show-traits", "--package-id=deck-of-playing-cards", "--format=text"], + packagePath: packageRoot, + configuration: data.config, + buildSystem: data.buildSystem, + ) + #expect(!textOutput.contains("trait1 - this trait is the default in app (default)")) + #expect(!textOutput.contains("trait2 - this trait is not the default in app")) + #expect(textOutput.contains("trait3")) + + (jsonOutput, _) = try await execute( + ["show-traits", "--package-id=deck-of-playing-cards", "--format=json"], + packagePath: packageRoot, + configuration: data.config, + buildSystem: data.buildSystem, + ) + json = try JSON(bytes: ByteString(encodingAsUTF8: jsonOutput)) + guard case .array(let contents) = json else { + Issue.record("unexpected result") + return + } + + #expect(1 == contents.count) + + guard case let first = contents.first else { + Issue.record("unexpected result") + return + } + guard case .dictionary(let trait3) = first else { + Issue.record("unexpected result") + return + } + #expect(trait3["name"]?.stringValue == "trait3") + } + } + @Test( .tags( .Feature.Command.Package.ShowExecutables,