diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 00000000000..fe04d0305fc --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,34 @@ +{ + "name": "Swift Nightly Main", + "image": "swiftlang/swift:nightly-main", + "features": { + "ghcr.io/devcontainers/features/common-utils:2": { + "installZsh": "false", + "username": "root", + "upgradePackages": "false" + }, + "ghcr.io/devcontainers/features/git:1": { + "version": "os-provided", + "ppa": "false" + } + }, + "postCreateCommand": "apt-get update && apt-get install -y curl sqlite3 libsqlite3-dev libncurses5-dev python3 build-essential", + "runArgs": [ + "--cap-add=SYS_PTRACE", + "--security-opt", + "seccomp=unconfined" + ], + "customizations": { + "vscode": { + "settings": { + "lldb.library": "/usr/lib/liblldb.so" + }, + "extensions": [ + "swiftlang.swift-vscode" + ] + } + }, + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + "remoteUser": "root" +} diff --git a/.dir-locals.el b/.dir-locals.el index f1abc19158e..bb0b979a20b 100644 --- a/.dir-locals.el +++ b/.dir-locals.el @@ -1,19 +1,5 @@ -;;; Directory Local Variables +;;; Directory Local Variables -*- no-byte-compile: t -*- ;;; For more information see (info "(emacs) Directory Variables") - -((nil - (eval . - ;; Auto-load the swiftpm project settings. - (unless (featurep 'swiftpm-project-settings) - (message "loading 'swiftpm-project-settings") - ;; Make sure the project's own utils directory is in the load path, - ;; but don't override any one the user might have set up. - (add-to-list - 'load-path - (concat - (let ((dlff (dir-locals-find-file default-directory))) - (if (listp dlff) (car dlff) (file-name-directory dlff))) - "Utilities/Emacs") - :append) - (require 'swiftpm-project-settings))))) +((nil . ((c-basic-offset . 4))) + (swift-mode . ((swift-mode:basic-offset . 4)))) diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000000..ac8a0df1bb3 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,13 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 4 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true +xcode_trim_whitespace_on_empty_lines = true + +[*.{yml,yaml}] +indent_size = 2 diff --git a/.github/ISSUE_TEMPLATE/BUG_REPORT.yml b/.github/ISSUE_TEMPLATE/BUG_REPORT.yml index 0c1f448e454..ac3bd5b4867 100644 --- a/.github/ISSUE_TEMPLATE/BUG_REPORT.yml +++ b/.github/ISSUE_TEMPLATE/BUG_REPORT.yml @@ -2,6 +2,14 @@ name: Bug Report description: Something isn't working as expected labels: [bug] body: +- type: checkboxes + id: cat-preferences + attributes: + label: "Is it reproducible with SwiftPM command-line tools: `swift build`, `swift test`, `swift package` etc?" + description: "Issues related to closed-source software are not tracked by this repository and will be closed. For Xcode and `xcodebuild`, please file a feedback at https://feedbackassistant.apple.com instead." + options: + - label: Confirmed reproduction steps with SwiftPM CLI. The description text _must_ include reproduction steps with either of command-line SwiftPM commands, `swift build`, `swift test`, `swift package` etc. + required: true - type: textarea attributes: label: Description diff --git a/.gitignore b/.gitignore index 5be3c7b3009..6a54c5657d8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ .build +.test +.index-build DerivedData /.previous-build xcuserdata @@ -18,3 +20,4 @@ Package.resolved .docc-build .vscode Utilities/InstalledSwiftPMConfiguration/config.json +Fixtures/BinaryLibraries/Static/Package1/Simple.artifactbundle/build diff --git a/.mailmap b/.mailmap index e63c4f1d675..5cb79e758e0 100644 --- a/.mailmap +++ b/.mailmap @@ -99,7 +99,7 @@ Tim Condon <0xTim@users.noreply.github.com> Tim Gymnich Tim Gymnich Tom Doron -Valeriy Van -Valeriy Van +Valeriy Van +Valeriy Van finagolfin finagolfin diff --git a/.swift-version b/.swift-version new file mode 100644 index 00000000000..4ac4fded49f --- /dev/null +++ b/.swift-version @@ -0,0 +1 @@ +6.2.0 \ No newline at end of file diff --git a/.swiftformat b/.swiftformat index 8ae09674179..4ee24410ab8 100644 --- a/.swiftformat +++ b/.swiftformat @@ -1,6 +1,6 @@ ## File options ---swiftversion 5.7 +--swiftversion 5.9 --exclude .build ## Formatting options diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/SwiftPM-Package.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/SwiftPM-Package.xcscheme deleted file mode 100644 index 447e5415e3b..00000000000 --- a/.swiftpm/xcode/xcshareddata/xcschemes/SwiftPM-Package.xcscheme +++ /dev/null @@ -1,911 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Benchmarks/Benchmarks/PackageGraphBenchmarks/PackageGraphBenchmarks.swift b/Benchmarks/Benchmarks/PackageGraphBenchmarks/PackageGraphBenchmarks.swift new file mode 100644 index 00000000000..09460dbd7e5 --- /dev/null +++ b/Benchmarks/Benchmarks/PackageGraphBenchmarks/PackageGraphBenchmarks.swift @@ -0,0 +1,183 @@ +@_spi(DontAdoptOutsideOfSwiftPMExposedForBenchmarksAndTestsOnly) +import Basics +import Benchmark +import Foundation +import PackageModel + +@_spi(DontAdoptOutsideOfSwiftPMExposedForBenchmarksAndTestsOnly) +import func PackageGraph.loadModulesGraph + +import Workspace + +let benchmarks = { + let defaultMetrics: [BenchmarkMetric] + if let envVar = ProcessInfo.processInfo.environment["SWIFTPM_BENCHMARK_ALL_METRICS"], + envVar.lowercased() == "true" || envVar == "1" { + defaultMetrics = .all + } else { + defaultMetrics = [ + .mallocCountTotal, + .syscalls, + ] + } + + let modulesGraphDepth: Int + if let envVar = ProcessInfo.processInfo.environment["SWIFTPM_BENCHMARK_MODULES_GRAPH_DEPTH"], + let parsedValue = Int(envVar) { + modulesGraphDepth = parsedValue + } else { + modulesGraphDepth = 150 + } + + let modulesGraphWidth: Int + if let envVar = ProcessInfo.processInfo.environment["SWIFTPM_BENCHMARK_MODULES_GRAPH_WIDTH"], + let parsedValue = Int(envVar) { + modulesGraphWidth = parsedValue + } else { + modulesGraphWidth = 150 + } + + let packagesGraphDepth: Int + if let envVar = ProcessInfo.processInfo.environment["SWIFTPM_BENCHMARK_PACKAGES_GRAPH_DEPTH"], + let parsedValue = Int(envVar) { + packagesGraphDepth = parsedValue + } else { + packagesGraphDepth = 10 + } + + // Benchmarks computation of a resolved graph of modules for a package using `Workspace` as an entry point. It runs PubGrub to get + // resolved concrete versions of dependencies, assigning all modules and products to each other as corresponding dependencies + // with their build triples, but with the build plan not yet constructed. In this benchmark specifically we're loading `Package.swift` + // for SwiftPM itself. + Benchmark( + "SwiftPMWorkspaceModulesGraph", + configuration: .init( + metrics: defaultMetrics, + maxDuration: .seconds(10), + thresholds: [ + .mallocCountTotal: .init(absolute: [.p90: 12000]), + .syscalls: .init(absolute: [.p90: 1600]), + ] + ) + ) { benchmark in + let path = try AbsolutePath(validating: #file).parentDirectory.parentDirectory.parentDirectory + let workspace = try Workspace(fileSystem: localFileSystem, location: .init(forRootPackage: path, fileSystem: localFileSystem)) + + for _ in benchmark.scaledIterations { + try workspace.loadPackageGraph(rootPath: path, observabilityScope: ObservabilitySystem.NOOP) + } + } + + // Benchmarks computation of a resolved graph of modules for a trivial synthesized package using `loadModulesGraph` + // as an entry point, which almost immediately delegates to `ModulesGraph.load` under the hood. + Benchmark( + "SyntheticModulesGraph", + configuration: .init( + metrics: defaultMetrics, + maxDuration: .seconds(10), + thresholds: [ + .mallocCountTotal: .init(absolute: [.p90: 17000]), + .syscalls: .init(absolute: [.p90: 5]), + ] + ) + ) { benchmark in + try syntheticModulesGraph( + benchmark, + modulesGraphDepth: modulesGraphDepth, + modulesGraphWidth: modulesGraphWidth + ) + } + + // Benchmarks computation of a resolved graph of modules for a synthesized package that includes macros, + // using `loadModulesGraph` as an entry point, which almost immediately delegates to `ModulesGraph.load` under + // the hood. + Benchmark( + "SyntheticModulesGraphWithMacros", + configuration: .init( + metrics: defaultMetrics, + maxDuration: .seconds(10), + thresholds: [ + .mallocCountTotal: .init(absolute: [.p90: 8000]), + .syscalls: .init(absolute: [.p90: 5]), + ] + ) + ) { benchmark in + try syntheticModulesGraph( + benchmark, + modulesGraphDepth: modulesGraphDepth, + modulesGraphWidth: modulesGraphWidth, + includeMacros: true + ) + } +} + +func syntheticModulesGraph( + _ benchmark: Benchmark, + modulesGraphDepth: Int, + modulesGraphWidth: Int, + includeMacros: Bool = false +) throws { + // If macros are included, modules are split in three parts: + // 1. top-level modules + // 2. macros + // 3. dependencies of macros + let macrosDenominator = includeMacros ? 3 : 1 + let libraryModules: [TargetDescription] = try (0..<(modulesGraphWidth / macrosDenominator)).map { i -> TargetDescription in + let dependencies = (0.. [TargetDescription.Dependency] in + if includeMacros { + [.target(name: "Module\(i)"), .target(name: "Macros\(i)")] + } else { + [.target(name: "Module\(i)")] + } + } + return try TargetDescription(name: "Module\(i)", dependencies: dependencies) + } + + let macrosModules: [TargetDescription] + let macrosDependenciesModules: [TargetDescription] + if includeMacros { + macrosModules = try (0..:SHELL:-package-name swift_package_manager>") +if(CMAKE_SYSTEM_NAME STREQUAL FreeBSD) + link_directories(/usr/local/lib) +endif() + +add_subdirectory(BuildSupport/SwiftSyntax) add_subdirectory(Sources) add_subdirectory(cmake/modules) diff --git a/CODEOWNERS b/CODEOWNERS index a45a3b347e9..692b1477a9d 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -2,15 +2,11 @@ # particular part of Swift are reviewed, either by themselves or by someone else. # They are also the gatekeepers for their part of Swift, with the final word on # what goes in or not. -# +# # The list is sorted by surname and formatted to allow easy grepping and # beautification by scripts. The fields are: name (N), email (E), web-address # (W), PGP key ID and fingerprint (P), description (D), and snail-mail address # (S). - -# N: Boris Buegling -# E: bbuegling@apple.com -# D: Package Manager # N: Tomer Doron # E: tomer@apple.com @@ -20,8 +16,16 @@ # E: m_desiatov@apple.com # D: Package Manager +# N: Ben Barham +# E: ben_barham@apple.com +# D: Package Manager + ### # The following lines are used by GitHub to automatically recommend reviewers. -* @neonichu @tomerd @MaxDesiatov +* @jakepetroules @dschaefer2 @bripeticca @plemarquand @owenv @bkhouri @cmcgee1024 @daveyc123 @rconnell9 + +Sources/Commands/PackageCommands/Migrate.swift @AnthonyLatsis @bnbarham @xedin @jakepetroules @dschaefer2 @bripeticca @plemarquand @owenv @bkhouri @cmcgee1024 @daveyc123 @rconnell9 +Sources/SwiftFixIt/* @AnthonyLatsis @bnbarham @xedin @jakepetroules @dschaefer2 @bripeticca @plemarquand @owenv @bkhouri @cmcgee1024 @daveyc123 @rconnell9 +Tests/SwiftFixItTests/* @AnthonyLatsis @bnbarham @xedin @jakepetroules @dschaefer2 @bripeticca @plemarquand @owenv @bkhouri @cmcgee1024 @daveyc123 @rconnell9 diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md deleted file mode 100644 index b0a0ab2f284..00000000000 --- a/CODE_OF_CONDUCT.md +++ /dev/null @@ -1,5 +0,0 @@ -# Code of Conduct - -The code of conduct for this project can be found at https://swift.org/code-of-conduct. - - diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b750b816b37..13763291b95 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,7 +2,7 @@ There are several types of contributions one can make. Bug fixes, documentation and enhancements that do not materially change the user facing semantics of Swift Package Manager should be submitted directly as PR. -Larger changes that do materially change the semantics of Swift Package Manager (e.g. changes to the manifest format or behavior) are required to go through [Swift Evolution Process](https://github.com/apple/swift-evolution/blob/master/process.md). +Larger changes that do materially change the semantics of Swift Package Manager (e.g. changes to the manifest format or behavior) are required to go through [Swift Evolution Process](https://github.com/swiftlang/swift-evolution/blob/master/process.md). To see how previous evolution decisions for SwiftPM have been made and have some direction for the development of future features please check out the [Community Proposals](https://forums.swift.org/tag/packagemanager). @@ -10,7 +10,7 @@ For more information about making contributions to the Swift project in general ## Reporting issues -Issues are tracked using [SwiftPM GitHub Issue Tracker](https://github.com/apple/swift-package-manager/issues). +Issues are tracked using [SwiftPM GitHub Issue Tracker](https://github.com/swiftlang/swift-package-manager/issues). Fill the following fields: @@ -37,7 +37,7 @@ generated and the Xcode build log. ## Setting up the development environment -First, clone a copy of SwiftPM code from https://github.com/apple/swift-package-manager. +First, clone a copy of SwiftPM code from https://github.com/swiftlang/swift-package-manager. If you are preparing to make a contribution you should fork the repository first and clone the fork which will make opening Pull Requests easier. See "Creating Pull Requests" section below. @@ -88,7 +88,47 @@ $> swift --version Apple Swift version 5.3 ``` -Note: Alternatively use tools like [swiftenv](https://github.com/kylef/swiftenv) that help manage toolchains versions. +Alternatively, there are tools like [swiftly](https://github.com/swiftlang/swiftly) that can install and manage toolchains automatically. This repository has a file called `.swift-version` that will keep swiftly at the current recommended version of the toolchain for best results. The `swiftly install` command ensures that SwiftPM's in-use toolchain is installed on your system and ready for you to do your development work with the usual swift commands. + +```bash +swiftly install +swift build +swift test +``` + +## Developing in the Linux devcontainer + +SwiftPM includes a devcontainer configuration that allows you to develop in a containerized environment with VS Code. This approach provides a consistent development environment with all necessary dependencies pre-installed, regardless of your host operating system. + +### Prerequisites + +1. Install [Visual Studio Code](https://code.visualstudio.com/) +2. Install the [Dev Containers extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) for VS Code +3. Install [Docker](https://www.docker.com/products/docker-desktop/) on your system + +### Opening the Project in a Container + +1. Clone the SwiftPM repository (if you haven't already) +2. Open the SwiftPM folder in VS Code +3. VS Code will detect the devcontainer configuration and prompt you to "Reopen in Container". Click this button to start building and opening the container + - Alternatively, you can press `F1`, type "Dev Containers: Reopen in Container", and press Enter +4. Wait for the container to build and initialize (this may take a few minutes the first time) + +### What's Included in the Dev Container + +The SwiftPM devcontainer is based on the `swiftlang/swift:nightly-main` Docker image and includes: + +- Swift nightly build from the main branch +- Git +- Common development utilities +- VS Code Swift extension for syntax highlighting and language support +- Debugging support with LLDB + +### Building and Testing in the Container + +Once you've opened the project in VS Code, choose `> Dev Containers: Build and Reopen in Container` from the command pallete. Once the container finishes building it will open and you can develop as if you are on Linux using VS Code. + +All the commands described in the "Local Development" section below will work in the container environment. ## Local Development @@ -174,26 +214,19 @@ Clone the following repositories beside the SwiftPM directory: $> git clone https://github.com/apple/swift-tools-support-core ``` -4. [Yams] and checkout tag with the [latest version](https://github.com/jpsim/Yams.git/tags) before 5.0.0. - - For example, if the latest tag is 4.0.6: - ```sh - $> git clone https://github.com/jpsim/yams --branch 4.0.6 - ``` - -5. [swift-driver] +4. [swift-driver] ```sh $> git clone https://github.com/apple/swift-driver ``` -6. [swift-system] and check out tag with the [latest version](https://github.com/apple/swift-system/tags). +5. [swift-system] and check out tag with the [latest version](https://github.com/apple/swift-system/tags). For example, if the latest tag is 1.0.0: ```sh $> git clone https://github.com/apple/swift-system --branch 1.0.0 ``` -7. [swift-collections] and check out tag with the [latest version](https://github.com/apple/swift-collections/tags). +6. [swift-collections] and check out tag with the [latest version](https://github.com/apple/swift-collections/tags). For example, if the latest tag is 1.0.1: ```sh @@ -217,6 +250,16 @@ Clone the following repositories beside the SwiftPM directory: $> git clone https://github.com/apple/swift-certificates ``` +10. [swift-syntax] + ```sh + $> git clone https://github.com/swiftlang/swift-syntax + ``` + +11. [swift-toolchain-sqlite] + ```sh + $> git clone https://github.com/swiftlang/swift-toolchain-sqlite + ``` + [swift-argument-parser]: https://github.com/apple/swift-argument-parser [swift-collections]: https://github.com/apple/swift-collections [swift-driver]: https://github.com/apple/swift-driver @@ -226,7 +269,8 @@ Clone the following repositories beside the SwiftPM directory: [swift-crypto]: https://github.com/apple/swift-crypto [swift-asn1]: https://github.com/apple/swift-asn1 [swift-certificates]: https://github.com/apple/swift-certificates -[Yams]: https://github.com/jpsim/yams +[swift-toolchain-sqlite]: https://github.com/swiftlang/swift-toolchain-sqlite +[swift-syntax]: https://github.com/swiftlang/swift-syntax #### Building @@ -314,33 +358,31 @@ Note there are several Linux and Swift versions options to choose from, e.g.: ## Creating Pull Requests -1. Fork: https://github.com/apple/swift-package-manager +1. Fork: https://github.com/swiftlang/swift-package-manager 2. Clone a working copy of your fork 3. Create a new branch 4. Make your code changes -5. Try to keep your changes (when possible) below 200 lines of code. -6. We use [SwiftFormat](https://www.github.com/nicklockwood/SwiftFormat) to enforce code style. Please install and run SwiftFormat before submitting your PR. -7. Commit (include the Radar link or GitHub issue id in the commit message if possible and a description your changes). Try to have only 1 commit in your PR (but, of course, if you add changes that can be helpful to be kept aside from the previous commit, make a new commit for them). -8. Push the commit / branch to your fork -9. Make a PR from your fork / branch to `apple: main` -10. While creating your PR, make sure to follow the PR Template providing information about the motivation and highlighting the changes. -11. Reviewers are going to be automatically added to your PR -12. Pull requests will be merged by the maintainers after it passes CI testing and receives approval from one or more reviewers. Merge timing may be impacted by release schedule considerations. +5. If a particular version of the Swift toolchain is needed then update the `.swift-version` file to that version (or use `swiftly use` to update it). +6. Try to keep your changes (when possible) below 200 lines of code. +7. We use [SwiftFormat](https://www.github.com/nicklockwood/SwiftFormat) to enforce code style. Please install and run SwiftFormat before submitting your PR, ideally isolating formatting changes only to code changed for the original goal of the PR. This will keep the PR diff smaller. +8. Commit (include the Radar link or GitHub issue id in the commit message if possible and a description your changes). Try to have only 1 commit in your PR (but, of course, if you add changes that can be helpful to be kept aside from the previous commit, make a new commit for them). +9. Push the commit / branch to your fork +10. Make a PR from your fork / branch to `apple: main` +11. While creating your PR, make sure to follow the PR Template providing information about the motivation and highlighting the changes. +12. Reviewers are going to be automatically added to your PR +13. Pull requests will be merged by the maintainers after it passes CI testing and receives approval from one or more reviewers. Merge timing may be impacted by release schedule considerations. By submitting a pull request, you represent that you have the right to license your contribution to Apple and the community, and agree by submitting the patch that your contributions are licensed under the [Swift license](https://swift.org/LICENSE.txt). +After a change is known not to cause regressions in the `main` branch, it may be considered for cherry-picking to the latest release branch depending on the release schedule. Cherry-picks require [a specific template to be followed](https://github.com/swiftlang/.github/blob/main/PULL_REQUEST_TEMPLATE/release.md) in PR description that consolidates information about the change necessary for inclusion in the release branch and provides risk evaluation for nominating the change. + ## Continuous Integration SwiftPM uses [swift-ci](https://ci.swift.org) infrastructure for its continuous integration testing. The bots can be triggered on pull-requests if you have commit access. Otherwise, ask one of the code owners to trigger them for you. -To run smoke test suite with the trunk compiler and other projects use: - -``` -@swift-ci please smoke test -``` This is **required** before a pull-request can be merged. @@ -348,9 +390,14 @@ This is **required** before a pull-request can be merged. To run just the self-hosted test suite (faster turnaround times so it can be used to get quick feedback) use: ``` -@swift-ci please smoke test self hosted +@swift-ci please test self hosted ``` +To run the windows self-hosted suite, use: + +``` +@swift-ci please test self hosted windows +``` To run the swift toolchain test suite including SwiftPM use: @@ -358,6 +405,13 @@ To run the swift toolchain test suite including SwiftPM use: @swift-ci please test ``` +To run the swift toolchain test suite against a specific platform use one of the following: + +``` +@swift-ci please test macos +@swift-ci please test linux +@swift-ci please test windows +``` To run package compatibility test suite (validates we do not break 3rd party packages) use: @@ -365,6 +419,7 @@ To run package compatibility test suite (validates we do not break 3rd party pac @swift-ci please test package compatibility ``` + ## Generating Documentation SwiftPM uses [DocC](https://github.com/apple/swift-docc) to generate some of its documentation (currently only the `PackageDescription` module). Documentation can be built using Xcode's GUI (Product → Build Documentation or `⌃⇧⌘D`) or manually: @@ -421,7 +476,7 @@ SwiftPM uses [Tools Support Core](https://github.com/apple/swift-tools-support-c If you want to connect with the Swift community you can: * Use Swift Forums: [https://forums.swift.org/c/development/SwiftPM](https://forums.swift.org/c/development/SwiftPM) -* Contact the CODEOWNERS: https://github.com/apple/swift-package-manager/blob/main/CODEOWNERS +* Contact the CODEOWNERS: https://github.com/swiftlang/swift-package-manager/blob/main/CODEOWNERS ## Additional resources diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 1888dd669ab..0a92b5bcc4c 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -291,7 +291,7 @@ needs to be listed here. - Quinn McHenry - Rahul Malik - Randy Becker <81446220+randy-becker@users.noreply.github.com> -- Rauhul Varma +- Rauhul Varma - Renzo Crisóstomo - Rich Ellis - Rick Ballard diff --git a/Documentation/ContinousIntegration.md b/Documentation/ContinousIntegration.md deleted file mode 100644 index 196e088cca8..00000000000 --- a/Documentation/ContinousIntegration.md +++ /dev/null @@ -1,26 +0,0 @@ -# Building Swift Packages or Apps that Use Them in Continuous Integration Workflows - -Build Swift packages with an existing continuous integration setup and prepare apps that consume package dependencies within an existing CI pipeline. - -## Overview - -*Continuous integration* (*CI*) is the process of automating and streamlining the building, analyzing, testing, archiving, and publishing of your apps to ensure that they're always in a releasable state. -Most projects that contain or depend on Swift packages don't require additional configuration. - -## Use the Expected Version of a Package Dependency - -To ensure the *CI* workflow’s reliability, make sure it uses the appropriate version of package dependencies. -SwiftPM records the result of dependency resolution in `Package.resolved` (at the top-level of the package) and it's used when performing dependency resolution (rather than having SwiftPM searching the latest eligible version of each package). Running `swift package update` updates all dependencies to the latest eligible versions and updates the `Package.resolved`. You can commit `Package.resolved` to your *Git* repository to ensure it’s always up-to-date on the *CI* environment to prevent the *CI* from building your project with unexpected versions of package dependencies. Otherwise you can choose to add `Package.resolved` file to `.gitignore` file and have `swift package resolve` command in charge of resolving the dependencies (`swift package resolve` is invoked by most SwiftPM commands). - -## Provide Credentials - -To resolve package dependencies that require authentication, such as private packages, you need to provide credentials to your CI setup. -SwiftPM honors the machine's SSH configuration - there's no additional setup required on SwiftPM side. For private package, use the SSH-based Git URLs and configure SSH credentials. You may also need to set up a known_hosts file in the ~/.ssh directory of the user that runs your CI tasks. - -CI services like [Jenkins](https://www.jenkins.io/doc/book/using/using-credentials), [Github Action](https://docs.github.com/en/free-pro-team@latest/actions/reference/authentication-in-a-workflow), [TravisCI](https://docs.travis-ci.com/user/private-dependencies), [CircleCI](https://circleci.com/docs/2.0/gh-bb-integration/#security) provide ways to set up SSH keys or other techniques to access private repositories. Since SwiftPM uses git to clone the repositories there's no additional setup required on SwiftPM side, and SwiftPM will honor the machine's SSH and Git configuration. - -## Using xcodebuild -When building on macOS based CI hosts you can use the command-line tool `xcodebuild`. -`xcodebuild` uses Xcode's built-in Git tooling to connect to repositories. In many cases, you don't need to make changes to how xcodebuild connects to them. However, some use cases require you use the configuration you set for your Mac's Git installation (Some examples: URL remapping, Proxy configurations, Advanced SSH configurations). To have xcodebuild use your Mac's Git installation and configuration instead of Xcode's built-in Git tooling, pass `-scmProvider system` to the xcodebuild command. - -For more information on using xcodebuild in continuous integration workflows, visit [this link](https://developer.apple.com/documentation/swift_packages/building_swift_packages_or_apps_that_use_them_in_continuous_integration_workflows). diff --git a/Documentation/Design/EvolutionIdeas.md b/Documentation/Design/EvolutionIdeas.md index e29c8be8c4f..cd445c4f1c4 100644 --- a/Documentation/Design/EvolutionIdeas.md +++ b/Documentation/Design/EvolutionIdeas.md @@ -13,7 +13,7 @@ If you're interested in participating in a particular evolution idea, please familiarize yourself with the existing discussion on that topic and start participating in the discussion thread of that idea. If a thread doesn't exist for that idea, please start one with a [draft -proposal](https://github.com/apple/swift-evolution/blob/master/proposal-templates/0000-swiftpm-template.md) +proposal](https://github.com/swiftlang/swift-evolution/blob/master/proposal-templates/0000-swiftpm-template.md) that can be used as a starting point. **Important Note**: This list is not in any particular order. I plan to keep @@ -190,7 +190,7 @@ Bug: N/A We need an easy way to edit the Package.swift manifest from automated tools, for cases where you don't want users to have to update the Swift code directly. We think that it's possible to provide an API to allow this, probably using -[`SwiftSyntax`](https://github.com/apple/swift/tree/master/tools/SwiftSyntax). +[`SwiftSyntax`](https://github.com/swiftlang/swift-syntax). Thread: N/A Bug: N/A diff --git a/Documentation/Design/PackageManagerCommunityProposal.md b/Documentation/Design/PackageManagerCommunityProposal.md deleted file mode 100644 index 6518fa0750c..00000000000 --- a/Documentation/Design/PackageManagerCommunityProposal.md +++ /dev/null @@ -1,311 +0,0 @@ -# Swift Package Manager Community Proposal - -> **PLEASE NOTE** This document represents the initial proposal for Swift Package Manger, and is provided for historical purposes only. It does not represent the current state or future direction of the project. For current documentation, see the main Swift Package Manager [documentation](../README.md). - ---- - -Package managers play a significant role in modern language ecosystems, and are as varied as the languages for which they are created. We designed the Swift Package Manager to solve the specific challenges of distributing and managing Swift code, drawing upon ideas we've seen from other systems and improving upon some of their shortcomings. - -This initial release is just a starting point, and we invite you to help us to build the best tool possible. To help you get started with the project, we have prepared the following Community Proposal. - -> For more information about contributing to Swift and the Swift Package Manager, see the ["Contributing" section of Swift.org](https://swift.org/contributing) - -* * * - -## Problem Statement - -The world is full of fascinating problems waiting to be solved. Software is created to solve some of these problems. - -Problems can be recursively decomposed into subproblems, such that any problem can be understood in terms of small, clearly defined tasks. Likewise, code can be organized into individual components, such that each one is responsible for a particular piece of functionality. - -No problem should ever have to be solved twice. By extracting code that solves a problem into a separate component, it can be reused in other situations where that problem arises. - -However, code reuse has an associated coordination cost. The question is how to minimize that cost. - -### Packaging - -Code must first be organized in a way that allows for reuse. We define a _package_ to be a grouping of software and associated resources intended for distribution. Packages are identified by a chosen name and may include additional information, such as a list of authors and license information. - -### Versioning - -As code is developed, it may add new features, remove existing features, or change underlying behavior. To track changes to code over time, a package defines an external _version number_, which corresponds to a particular revision. A version number typically takes the form of `MAJOR.MINOR.PATCH`, which semantically identifies different versions of the same package. - -### Dependencies - -A package may also specify one or more other packages as _dependencies_, which are required to build the package. When a package declares a dependency, it may constrain the dependent package to a subset of available versions by specifying a set of _requirements_. For example, an `Orchestra` package may specify a `Cello` package as a dependency, with the requirement that the dependency's version is at least `2.0.0`. - -### "Dependency Hell" - -When a project's packages have requirements that conflict with one another, it creates a situation known colloquially as _"dependency hell"_. This term is used to describe a number of different problems that may arise as new requirements are added to a project, such as inappropriate versioning and incompatible version requirements. Such situations often significantly decrease developer productivity and may prevent the adoption of security fixes and other important changes in updated versions of a dependency. - -### Resolution - -We believe that the best solution to the problems stated above is a _package manager_ --- a tool that automates the processes of downloading packages, building and linking package modules, and resolving dependencies. Such a tool can take steps to prevent and mitigate certain forms of dependency hell. And for situations that cannot be avoided, it can provide tools to clearly diagnose problems when they arise. The tool should adapt a flexible distribution and collaboration model, rooted in strong conventions and sensible defaults, making it easy to use and well-suited to the needs of developers. - -* * * - -## Aspects of the Design - -Although the Swift Package Manager is still an early work-in-progress, many of our design goals are reflected in the current product. We'd like to specifically call out the following design decisions: - -- A Build System for Swift Packages -- Convention-Based Configuration -- Swift Code as Manifest Format -- Explicit Dependency Declaration -- Packages and Modules -- System Library Access with Module Maps -- Semantic Versioning -- Build Environment Isolation -- Source-Based Distribution -- Package Decentralization - -Creating an infinitely flexible tool to satisfy every conceivable use case is specifically a _non-goal_ of this project. Instead, we are prioritizing conventions and designs that minimize the friction for distributing Swift code for individuals, and promote the development of a healthy package ecosystem for the community. - -### A Build System for Swift Packages - -Swift is a compiled language. As such, the Swift Package Manager provides a _build system_ for Swift (`swift build`), which knows how to invoke build tools, like the Swift compiler (`swiftc`), to produce built products from Swift source files. - -By design, the Swift Package Manager tightly integrates features across package definitions and the Swift compiler. This allows for the build system to introspect package definitions and use insights from the compiler to manage their configuration. - -### Convention-Based Configuration - -Rather than requiring that every detail of a package is explicitly configured, the Swift Package Manager establishes a set of conventions about how packages are structured. - -A package is simply a directory containing a manifest file, called `Package.swift`. By default, the Swift Package Manager will automatically infer the information it needs to build the package from the layout of the directory itself: - -* Any source code files in the root directory or in a top-level `Sources/` or `src/` directory will be automatically included by the build system. -* Any subdirectories of the `Sources/` or `src/` directory will automatically define separate modules. -* If the root directory contains a file called `main.swift`, that file will be used to create an executable with the name of the package. - -Taking this approach also has the benefit of allowing this default behavior to evolve and improve over time without requiring any changes to existing packages. - -The manifest format will eventually allow for additional configuration, such as any dependencies the package has or any custom build flags to set on individual source files, to accommodate any deviation from conventional expectations. - -### Swift Code as Manifest Format - -The manifest file, `Package.swift`, defines a `Package` object in Swift code. - -Using Swift as a manifest file format allows us to provide a great authoring experience with the tools you already use to work with Swift. However, this could make it difficult for tools to automatically modify the manifest. To mitigate this issue, the manifest file format will eventually be more structured. Although it will remain valid Swift code, the manifest file will be divided into a declarative section, which is easily machine-editable, and an optional section of additional code, which can be ignored by tools. - -The manifest file is used to specify any configuration that isn't expressed by convention. Currently, any dependencies a package has are declared in its manifest. In the future, additional configuration, such as custom build flags, will be supported as well. Packages that do not conform to the conventional-based structure will also be able to specify their source files explicitly. - -Unlike some other build systems, the provided APIs do not interact with the project build environment directly. This allows the Swift build system to evolve over time, without breaking existing packages. Instead, the APIs provided are used to create a declarative model of your package, which is then used by the Swift Package Manager to build the products. By defining a clear, descriptive API for defining packages, we allow developers to communicate their intent while still allowing the Package Manager to evolve. - -### Explicit Dependency Declaration - -Package dependencies are explicitly declared in the manifest, each specifying a source URL and version requirements. - -The source URL corresponds to a Git repository that is accessible to the user building the package. The version requirements correspond to tagged releases in the repository. - -When a package is built, the sources of its dependencies are cloned from their respective repository URLs as needed. This process continues with any sub-dependencies, until all of the dependent packages are found. Next, the version requirements of each dependency declaration are resolved. If a dependency has no explicit version requirements, the most recent version is used. - -### Packages and Modules - -Swift organizes code into _modules_. Each module specifies a namespace and enforces access control on which parts of that code can be used outside of that module. - -By default, the module name of each dependency is derived from its source URL. For example, a dependency with the source URL `git://path/to/PlayingCard.git` would be imported in code with `import PlayingCard`. A custom module name for a dependency can be specified in its declaration in the manifest. - -### System Library Access with Module Maps - -A package may depend on one or more _system module packages_, which allow system libraries written in C to be imported and used in Swift code. - -Like any package, a system module package must contain a `Package.swift` file. However, instead of Swift source files, a system module package contains only a `module.modulemap` file, which maps the headers of the system library. - -### Semantic Versioning - -Swift packages are expected to follow [Semantic Versioning](http://semver.org) (SemVer), a standard for assigning version numbers to software releases. - -With SemVer, a version number takes the form `MAJOR.MINOR.PATCH`, where `MAJOR`, `MINOR`, and `PATCH` are non-negative integers. You increment the `MAJOR` version when you make an incompatible API change, the `MINOR` version when you add functionality in a backwards-compatible manner, and the `PATCH` version when you make backwards-compatible bugfixes. When you increment the `MINOR` version, the `PATCH` version is reset to `0`, and when you increment the `MAJOR` version, both the `MINOR` and `PATCH` versions are reset to `0`. - -Each release corresponds to a commit in the repository that is tagged with a version number. To see all of the releases of a package, use the `git tag` command with no arguments: - -```shell -$ git tag -1.0.0 -1.0.1 -1.0.2 -1.1.0 -1.1.1 -``` - -To create a new release, use the `git tag` command passing the version number as the first argument: - -```shell -$ git tag 2.0.0 -``` - -By adopting a common versioning convention, package maintainers can more clearly communicate the impact a new version of their code will have, and developers can better understand the changes between versions of a package. - -### Build Environment Isolation - -A package exists in a self-contained directory containing cloned dependency sources and built products. Each package directory is considered independently from the rest of the file system. - -Developing software in isolation, with all dependencies explicitly declared, ensures that even packages with complex requirements can be reliably built and deployed to different environments. Implicit dependencies on the availability of system libraries, with the exception of platform-standard libraries, is strongly discouraged. - -### Source-Based Distribution - -Packages are distributed and consumed as source code, rather than pre-compiled binaries. - -Although it requires additional computational resources, this approach guarantees that developers can adopt new features on platforms they support, without being reliant on vendors to supply updated dependencies. This also has the advantage of allowing tools to do things like automated testing and API analysis of package dependencies. - -### Package Decentralization - -There is no single centralized index of packages. - -A package's metadata and dependencies are specified in its manifest file, which is stored along with the code in its repository. Any package whose source URL is accessible to the current user can be used as a dependency for any other package. - -Indexes can be created to aid in the discoverability and curation of Swift packages, without compromising the flexibility and freedom afforded by decentralization. For example, a package could easily switch between a canonical version of a package from an index to a private fork, simply by changing the source URL of the dependency declaration. - -* * * - -## Future Features - -Again, this initial release of the Swift Package Manager is just a starting point. Here is a list of some features we'd like to see in future releases (in no particular order): - -- Automated Testing -- Documentation Generation -- Cross-Platform Packages -- Support for Other Languages -- Support for Other Build Systems -- Support for Other Version Control Systems -- Library-Based Architecture -- Standardized Licensing -- Security and Signing -- A Package Index -- Dynamic Libraries -- Enforced Semantic Versioning -- Importing Dependencies by Source URL -- Packaging Resources -- Module Interdependency Determination -- Dependency Resolution -- Resource Management -- Package Flavors -- User-Global Installation - -Many of these ideas will eventually become concrete proposals following the [Swift evolution process](https://github.com/apple/swift-evolution). We welcome your input on which features to prioritize or how we might design and implement them. And we're excited to hear about any other ideas for new features. - -### Automated Testing - -We would like to add support for building and running automated tests for packages. - -This should be supported in a standardized way to encourage all packages to provide tests and allow users to easily run the tests for all of a package's dependencies. A package index could use the testing support as a way to validate package submissions to the index. An automated test harness for known packages could also allow the Swift project to validate changes to the Swift compiler, to the Swift Package Manager, or to popular packages with many clients. - -The Swift Package Manager may explicitly support [XCTest](https://swift.org/core-libraries/#xctest), from the Swift Core Libraries, as the native testing library. - -### Documentation Generation - -We would like to establish a standard for documenting packages and provide tools for automatically generating that documentation. - -Package maintainers would be encouraged to adequately document their packages, and to provide their documentation in a standardized manner. In addition, a package index could, as part of the submission process, serve generated documentation alongside the package. - -### Cross-Platform Packages - -We intend to improve the authoring experience for packages that work across multiple platforms (e.g. Linux and OS X). - -Currently this is difficult for nontrivial packages, as the current support for system modules often requires a package to depend on a specific module map with platform-specific header paths. - -### Support for Other Languages - -We would like to add support to the Swift Package Manager for languages other than Swift. This includes support for mixing and matching Swift with other languages. - -Specifically, we are most interested in support for C-based languages, because the existing ecosystem of Swift code is heavily reliant on C-based code --- up to and including Objective-C runtime support in Swift itself on Apple platforms. - -### Support for Other Build Systems - -We are considering supporting hooks for the Swift Package Manager to call out to other build systems, and/or to invoke shell scripts. - -Adding this feature would further improve the process of adapting existing libraries for use in Swift code. - -We intend to tread cautiously here, as incorporating shell scripts and other external build processes significantly limits the ability of the Swift Package Manager to understand and analyze the build process. - -### Support for Other Version Control Systems - -We are investigating ways to support version control systems other than Git for the distribution of Swift packages. - -Any version control system that allows source code to be addressed by a single URL and has some mechanism for tagged releases should be supported. - -### Library-Based Architecture - -Currently, the Swift Package Manager is packaged as a command line interface, exposed as a subcommand of the `swift` command. In the future, we would like to make it available as a library with a clearly defined API, so that other tools can more easily be built on top of it. - -We would also like to make it possible for an IDE to control Package Manager workflow, such as updating a package's dependencies to the latest versions. All of the major features of the package manager should be exposed through these APIs, allowing great integration with IDEs like Xcode. - -### Standardized Licensing - -We would like to provide a mechanism for managing the licenses of dependencies. - -The Swift Package Manager could check the license(s) of each package in the dependency tree, and verify that all of them fall within a specified acceptance policy. For example, a package may specify that all of its dependencies must have at least one license specified, or that none of its dependencies are licensed with certain licenses. Some licenses are known to be incompatible, and the package manager should be able to flag such issues. - -### Security and Signing - -We would like to provide a built-in security mechanism to sign and verify packages. - -This would ensure that packages are not altered after publication. By default, the package manager might reject any remote packages that aren't signed, with an option to override this behavior. - -We may additionally incorporate a chain of trust mechanism to validate the source of a package. The Swift Package Manager could be configured to accept only packages signed by a valid signing certificate in the chain of trust of a trusted authority. - -### A Package Index - -Although the Swift Package Manager is designed to be decentralized, there are certain advantages to centralized package indexes. - -Centralized indexes can aid in the discoverability and curation of Swift packages. They can be used to host source code, generate and serve documentation, run automated tests and code analyzers, or visualize changes to APIs over time. For example, by analyzing the interfaces of all submitted packages, an index could allow maintainers to identify the impact of any change to the public API of a package to any registered packages that depend on it. - -The index could also act as a naming authority, designating certain packages with canonical names. As a naming authority and software distributor, a centralized index would have a responsibility to ensure the integrity of packages, the security of identities, and the availability of resources, as well as the ability to revoke fraudulent or malicious packages. - -We would like to provide a package index in the future, and are investigating possible solutions. - -### Dynamic Libraries - -We would like to add support for dynamic libraries (and, at least on OS X, framework bundles). - -By default, we plan to continue to build packages as static libraries, which incur less runtime overhead than dynamic libraries. - -### Enforced Semantic Versioning - -We would like to be able to automatically detect changes to the public API of a package as a way to help maintainers select an appropriate semantic version component for each release. - -For example, if you change only the implementation of a method, a new `PATCH` version would be allowed, as this change is unlikely to break dependent packages. If a new method is added to the public API, the `MINOR` version should be updated. If you remove a method from the public API or change a public method's signature, the Swift Package Manager would require the next version to update the `MAJOR` version. By doing so, maintainers can avoid inadvertently pushing incompatible release of their packages, and consumers can regularly upgrade packages for security or performance improvements without fear of breaking changes to the API. - -Relatedly, we may decide at some point in the future to restrict packages from pulling in dependencies which do not yet define a public API, as indicated by a `1.0.0` release. If this restriction is imposed, it would be possible to override these requirements in your local manifest, thereby allowing development of pre-1.0 packages, without encouraging their proliferation. - -#### Importing Dependencies by Source URL - -In the future, we may allow remote packages to be imported into source files by passing a source URL to an `import` statement. - -This would allow developers to quickly evaluate packages with no setup cost. This would also allow Swift scripts to make use of the packaging ecosystem. The one restriction would be that, in order to publish a package, all `import` statements would have to be resolved to a versioned dependency declared in the manifest. - -### Module Interdependency Determination - -Currently, if you add `import B` to a source file in module A, you would also need to specify this dependency in the manifest file. If the dependency is not specified, builds will fail because module B must be built before module A. We would like to provide a command that would calculate your module inter-dependencies and alter the machine editable portion of `Package.swift` for you. - -It may be possible for the package manager to calculate this every build, but doing so might introduce significant overhead to the build process. - -### Dependency Resolution - -The Swift Package Manager does not currently provide a mechanism for automatically resolving conflicts in a dependency tree. However, this will be provided in the future. - -### Resource Management - -OS X provides a resource management solution for libraries in the form of framework bundles. We would like to provide a cross-platform solution for packages that have resources to manage. - -This might involve bringing frameworks to Linux. It could instead mean that packages with resources are built as frameworks on OS X, but are built on Linux as libraries with some associated autogenerated glue code for accessing resources. - -### Package Flavors - -We may wish to consider supporting "flavors" of a package (similar to the condition sets that Xcode calls "Build Configurations"). - -One type of flavor is along predefined axes, such as platform or architecture. - -We could also chose to support user-defined flavors. For example, a graphics library might offer a high-precision version and a fast-math version. User-defined flavors are problematic, as they can easily lead to "dependency hell" situations, such as if two packages of a dependency tree require different flavors of the same package. - -### User-Global Installation - -In the future, the Swift Package Manager may allow packages to be built and installed to a "user-global" directory located in the current user's home directory, which could be used to build utilities for ad-hoc use. - -* * * - -If you have any questions about this document, or would like to share any thoughts about existing features, please contact the mailing list for Swift Package Manager development: - -For information about contributing to Swift or the Swift Package Manager, check out ["Contributing to Swift" on Swift.org](https://swift.org/contributing). - -If you want to discuss new or planned features, see the [Swift evolution process](https://swift.org/contributing#participating-in-the-swift-evolution-process). diff --git a/Documentation/Design/README.md b/Documentation/Design/README.md index 952797c5b35..cc0d8f27c97 100644 --- a/Documentation/Design/README.md +++ b/Documentation/Design/README.md @@ -1,6 +1,6 @@ # Swift Package Manager Design Docs Swift Package Manager design is done in open source, and governed by Swift Evolution process. -As such, you can find up-to-date Swift Package Manager design docs [in the Swift Evolution repository](https://github.com/apple/swift-evolution/tree/main/proposals). +As such, you can find up-to-date Swift Package Manager design docs [in the Swift Evolution repository](https://github.com/swiftlang/swift-evolution/tree/main/proposals). This directory contains documentation on the engineering design decisions and internals of the Swift Package Manager, outside the evolution process. diff --git a/Documentation/Design/SwiftBasedManifestFormat.md b/Documentation/Design/SwiftBasedManifestFormat.md index 381f5bd01fe..917946b1f64 100644 --- a/Documentation/Design/SwiftBasedManifestFormat.md +++ b/Documentation/Design/SwiftBasedManifestFormat.md @@ -1,6 +1,6 @@ # Swift-based Manifest Format -> **PLEASE NOTE** This document represents the initial proposal for Swift Package Manger, and is provided for historical purposes only. It does not represent the current state or future direction of the project. For current documentation, see the main Swift Package Manager [documentation](../README.md). +> **PLEASE NOTE** This document represents the initial proposal for Swift Package Manager, and is provided for historical purposes only. It does not represent the current state or future direction of the project. For current documentation, see the main Swift Package Manager [documentation](../README.md). ## Purpose @@ -122,7 +122,7 @@ It is important to note that even when using this feature, package manifest stil The package definition format being written in Swift is problematic for tools that wish to perform automatic updates to the file (for example, in response to a user action, or to bind to a user interface), or for situations where dealing with executable code is problematic. -To that end, the declarative package specification portion of the file is "Swift" in the same sense that "JSON is Javascript". The syntax itself is valid, executable, Swift but the tools that process it will only accept a restricted, declarative, subset of Swift which can be statically evaluated, and which can be unambiguously, automatically rewritten by editing tools. We do not intend to define a "standard" syntax for "Swift Object Notation", but we intend to accept a natural restriction of the language which only accepts literal expressions. We do intend to allow the restricted subset to take full advantage of Swift's rich type inference and literal convertable design to allow for a succinct, readable, and yet expressive syntax. +To that end, the declarative package specification portion of the file is "Swift" in the same sense that "JSON is Javascript". The syntax itself is valid, executable, Swift but the tools that process it will only accept a restricted, declarative, subset of Swift which can be statically evaluated, and which can be unambiguously, automatically rewritten by editing tools. We do not intend to define a "standard" syntax for "Swift Object Notation", but we intend to accept a natural restriction of the language which only accepts literal expressions. We do intend to allow the restricted subset to take full advantage of Swift's rich type inference and literal convertible design to allow for a succinct, readable, and yet expressive syntax. The customization section above will *not* be written in this syntax. Instead, the customization section will be clearly demarcated in the file. The leading file section up to the first '// MARK:' will be processed as part of the restricted declarative specification. All subsequent code **must be** honored by tools which only need to consume the output of the specification, and **should be** displayed by tools which present an editor view of the manifest, but **should not** be automatically modified. The semantics of the APIs will be specifically designed to accommodate the expected use case of editor support for the primary data with custom project-specific logic for special cases. diff --git a/Documentation/ModuleAliasing.md b/Documentation/ModuleAliasing.md deleted file mode 100644 index 935f73a6f67..00000000000 --- a/Documentation/ModuleAliasing.md +++ /dev/null @@ -1,202 +0,0 @@ -# Module Aliasing by SwiftPM - -## Overview - -The number of package dependencies often grows, with that, a name collision can occur among modules from different packages. Module names such as `Logging` or `Utils` are common examples. In order to resolve the collision, SwiftPM (in 5.7+) introduces a new parameter `moduleAliases`, which allows a user to define new unique names for the conflicting modules without requiring any source code changes. - -## How to Use - -Let's consider the following scenarios to go over how module aliasing can be used. - -### Example 1 - -`App` imports a module called `Utils` from a package `swift-draw`. It wants to add another package dependency `swift-game` and imports a module `Utils` vended from the package. - -``` - App - |— Module Utils (from package ‘swift-draw’) - |— Module Utils (from package ‘swift-game’) -``` - -Package manifest `swift-game` -``` -{ - name: "swift-game", - products: [ - .library(name: "Utils", targets: ["Utils"]), - ], - targets: [ - .target(name: "Utils", dependencies: []) - ] -} -``` - -Package manifest `swift-draw` -``` -{ - name: "swift-draw", - products: [ - .library(name: "Utils", targets: ["Utils"]), - ], - targets: [ - .target(name: "Utils", dependencies: []) - ] -} -``` - -Both `swift-draw` and `swift-game` vend modules with the same name `Utils`, thus causing a conflict. To resolve the collision, a new parameter `moduleAliases` can now be used to disambiguate them. - -Package manifest `App` -``` - targets: [ - .executableTarget( - name: "App", - dependencies: [ - .product(name: "Utils", - package: "swift-draw"), - .product(name: "Utils", - package: "swift-game", - moduleAliases: ["Utils": "GameUtils"]), - ]) - ] -``` - -The value for the `moduleAliases` parameter is a dictionary where the key is the original module name in conflict and the value is a user-defined new unique name, in this case `GameUtils`. This will rename the `Utils` module in package `swift-game` as `GameUtils`; the name of the binary will be `GameUtils.swiftmodule`. No source or manifest changes are required by the `swift-game` package. - -To use the aliased module, `App` needs to reference the the new name, i.e. `import GameUtils`. Its existing `import Utils` statement will continue to reference the `Utils` module from package `swift-draw`, as expected. - -Note that the dependency product names are duplicate, i.e. both have the same name `Utils`, which is by default not allowed. However, this is allowed when module aliasing is used as long as no multiple files with the same product name are created. This means they must all be automatic library types, or at most one of them can be a static library, dylib, an executable, or any other type that creates a file or a directory with the product name. - -### Example 2 - -`App` imports a module `Utils` from a package `swift-draw`. It wants to add another package dependency `swift-game` and imports a module `Game` vended from the package. The `Game` module imports `Utils` from the same package. - -``` -App - |— Module Utils (from package ‘swift-draw’) - |— Module Game (from package ‘swift-game’) - |— Module Utils (from package ‘swift-game’) -``` - -Package manifest `swift-game` -``` -{ - name: "swift-game", - products: [ - .library(name: "Game", targets: ["Game"]), - ], - targets: [ - .target(name: "Game", dependencies: ["Utils"]) - .target(name: "Utils", dependencies: []) - ] -} -``` - -Similar to Example 1, both packages contain modules with the same name `Utils`, thus causing a conflict. Although `App` does not directly import `Utils` from `swift-game`, the conflicting module still needs to be disambiguated. - -We can use `moduleAliases` again, as follows. - -Package manifest `App` -``` - targets: [ - .executableTarget( - name: "App", - dependencies: [ - .product(name: "Utils", - package: "swift-draw"), - .product(name: "Game", - package: "swift-game", - moduleAliases: ["Utils": "GameUtils"]), - ]) - ] -``` - -The `Utils` module from `swift-game` is renamed as `GameUtils`, and all the references to `Utils` in source files of `Game` are compiled as `GameUtils`. Similar to Example 1, no source or manifest changes are required by the `swift-game` package. - -If more aliases need to be defined, they can be added with a comma delimiter, per below. - -``` - moduleAliases: ["Utils": "GameUtils", "Logging": "GameLogging"]), -``` - -## Override Module Aliases - -If module alias values defined upstream are conflicting downstream, they can be overriden by chaining; add an entry to the `moduleAliases` parameter downstream using the conflicting alias value as a key and provide a unique value. - -To illustrate, the `swift-draw` and `swift-game` packages are modified to have the following dependencies and module aliases. - -Package manifest `swift-draw` -``` -{ - name: "swift-draw", - dependencies: [ - .package(url: https://.../a-utils.git), - .package(url: https://.../b-utils.git), - ], - products: [ - .library(name: "Draw", targets: ["Draw"]), - ], - targets: [ - .target(name: "Draw", - dependencies: [ - .product(name: "Utils", - package: "a-utils", - moduleAliases: ["Utils": "FooUtils"]), - .product(name: "Utils", - package: "b-utils", - moduleAliases: ["Utils": "BarUtils"]), - ]) - ] -} -``` -Package manifest `swift-game` -``` -{ - name: "swift-game", - dependencies: [ - .package(url: https://.../c-utils.git), - .package(url: https://.../d-utils.git), - ], - products: [ - .library(name: "Game", targets: ["Game"]), - ], - targets: [ - .target(name: "Game", - dependencies: [ - .product(name: "Utils", - package: "c-utils", - moduleAliases: ["Utils": "FooUtils"]), - .product(name: "Utils", - package: "d-utils", - moduleAliases: ["Utils": "BazUtils"]), - ]) - ] -} -``` - -Both packages define `FooUtils` as an alias, thus causing a conflict downstream. -To override it, the `App` manifest can define its own module aliases per below. -``` - targets: [ - .executableTarget( - name: "App", - dependencies: [ - .product(name: "Draw", - package: "swift-draw", - moduleAliases: ["FooUtils": "DrawUtils"]), - .product(name: "Game", - package: "swift-game", - moduleAliases: ["FooUtils": "GameUtils"]), - ]) - ] -``` -The `Utils` module from package `a-utils` will be renamed as `DrawUtils`, and `Utils` from package `c-utils` will be renamed as `GameUtils`. Each overriden alias will be applied to all of the targets that depend on each module. - -## Requirements - -* A package needs to adopt the swift tools version 5.7 and above to use the `moduleAliases` parameter. -* A module being aliased needs to be a pure Swift module only: no ObjC/C/C++/Asm are supported due to a likely symbol collision. Similarly, use of `@objc(name)` should be avoided. -* A module being aliased cannot be a prebuilt binary due to the impact on mangling and serialization, i.e. source-based only. -* A module being aliased should not be passed to a runtime call such as `NSClassFromString(...)` that converts (directly or indirectly) String to a type in a module since such call will fail. -* If a target mapped to a module being aliased contains resources, they should be asset catalogs, localized strings, or resources that do not require explicit module names. -* If a product that a module being aliased belongs to has a conflicting name with another product, at most one of the products can be a non-automatic library type. diff --git a/Documentation/PackageCollections.md b/Documentation/PackageCollections.md deleted file mode 100644 index 6e8fbde4f29..00000000000 --- a/Documentation/PackageCollections.md +++ /dev/null @@ -1,374 +0,0 @@ -# Package Collections - -Package collections, introduced by [SE-0291](https://github.com/apple/swift-evolution/blob/main/proposals/0291-package-collections.md), are -curated lists of packages and associated metadata that make discovery of existing packages easier. They are authored as static JSON documents -and can be published to the web or distributed to local file systems. - -## Table of Contents - -- [Using package collections with the `package-collection` CLI](#using-package-collections) - - [`add` subcommand](#add-subcommand) - - [Add signed package collections](#signed-package-collections) - - [Configure trusted root certificates](#trusted-root-certificates) - - [Add unsigned package collections](#unsigned-package-collections) - - [`describe` subcommand](#describe-subcommand) - - [Describe a package collection](#metadata-and-packages-of-a-collection) - - [Describe a package](#metadata-of-a-package) - - [Describe a package version](#metadata-of-a-package-version) - - [`list` subcommand](#list-subcommand) - - [`refresh` subcommand](#refresh-subcommand) - - [`remove` subcommand](#remove-subcommand) - - [`search` subcommand](#search-subcommand) - - [Keyword search](#string-based-search) - - [Module search](#module-based-search) -- [Package collection configuration](#configuration-file) -- [Publishing package collections](#publishing-package-collections) - - [Creating package collections](#creating-package-collections) - - [Signing package collections](#package-collection-signing-optional) - - [Protecting package collections](#protecting-package-collections) - -## Using package collections - -With the `swift package-collection` command-line interface, SwiftPM users can subscribe to package collections. Contents of imported package -collections are accessible to any clients of [libSwiftPM](libSwiftPM.md). - -`swift package-collection` has the following subcommands: -- [`add`](#add-subcommand): Add a new collection -- [`describe`](#describe-subcommand): Get metadata for a collection or a package included in an imported collection -- [`list`](#list-subcommand): List configured collections -- [`refresh`](#refresh-subcommand): Refresh configured collections -- [`remove`](#remove-subcommand): Remove a configured collection -- [`search`](#search-subcommand): Search for packages by keywords or module names within imported collections - -### `add` subcommand - -This subcommand adds a package collection hosted on the web (HTTPS required): - -```bash -$ swift package-collection add https://www.example.com/packages.json -Added "Sample Package Collection" to your package collections. -``` - -Or found in the local file system: - -```bash -$ swift package-collection add file:///absolute/path/to/packages.json -Added "Sample Package Collection" to your package collections. -``` - -The optional `order` hint can be used to order collections and may potentially influence ranking in search results: - -```bash -$ swift package-collection add https://www.example.com/packages.json [--order N] -Added "Sample Package Collection" to your package collections. -``` - -#### Signed package collections - -Package collection publishers may sign a collection to protect its contents from being tampered with. If a collection is signed, SwiftPM will check that the -signature is valid before importing it and return an error if any of these fails: -- The file's contents, signature excluded, must match what was used to generate the signature. In other words, this checks to see if the collection has been altered since it was signed. -- The signing certificate must meet all the [requirements](#requirements-on-signing-certificate). - -```bash -$ swift package-collection add https://www.example.com/bad-packages.json -The collection's signature is invalid. If you would like to continue please rerun command with '--skip-signature-check'. -``` - -Users may continue adding the collection despite the error or preemptively skip the signature check on a package collection by passing the `--skip-signature-check` flag: - -```bash -$ swift package-collection add https://www.example.com/packages.json --skip-signature-check -``` - -For package collections hosted on the web, publishers may ask SwiftPM to [enforce the signature requirement](#protecting-package-collections). If a package collection is -expected to be signed but it isn't, user will see the following error message: - -```bash -$ swift package-collection add https://www.example.com/bad-packages.json -The collection is missing required signature, which means it might have been compromised. -``` - -Users should NOT add the package collection in this case. - -##### Trusted root certificates - -Since generating a collection signature requires a certificate, part of the signature check involves validating the certificate and its chain and making sure that the root certificate is trusted. - -On Apple platforms, all root certificates that come preinstalled with the OS are automatically trusted. Users may include additional certificates to trust by placing -them in the `~/.swiftpm/config/trust-root-certs` directory. - -On non-Apple platforms, there are no trusted root certificates by default other than those shipped with the [certificate-pinning configuration](#protecting-package-collections). Only those -found in `~/.swiftpm/config/trust-root-certs` are trusted. This means that the signature check will always fail unless the `trust-root-certs` directory is set up: - -```bash -$ swift package-collection add https://www.example.com/packages.json -The collection's signature cannot be verified due to missing configuration. -``` - -Users can explicitly specify they trust a publisher and any collections they publish, by obtaining that publisher's root certificate and saving it to `~/.swiftpm/config/trust-root-certs`. The -root certificates must be DER-encoded. Since SwiftPM trusts all certificate chains under a root, depending on what roots are installed, some publishers may already be trusted implicitly and -users don't need to explicitly specify each one. - -#### Unsigned package collections - -Users will get an error when trying to add an unsigned package collection: - -```bash -$ swift package-collection add https://www.example.com/packages.json -The collection is not signed. If you would still like to add it please rerun 'add' with '--trust-unsigned'. -``` - -To continue user must confirm their trust by passing the `--trust-unsigned` flag: - -```bash -$ swift package-collection add https://www.example.com/packages.json --trust-unsigned -``` - -The `--skip-signature-check` flag has no effects on unsigned collections. - -### `describe` subcommand - -This subcommand shows metadata for a collection or a package included in an imported collection. The result can optionally be returned as JSON using `--json` for -integration into other tools. - -#### Metadata and packages of a collection - -`describe` can be used for both collections that have been previously added to the list of the user's configured collections, as well as to preview any other collections. - -```bash -$ swift package-collection describe [--json] https://www.example.com/packages.json -Name: Sample Package Collection -Source: https://www.example.com/packages.json -Description: ... -Keywords: best, packages -Created At: 2020-05-30 12:33 -Packages: - https://github.com/jpsim/yams - ... -``` - -##### Signed package collections - -If a collection is signed, SwiftPM will check that the signature is valid before showing a preview. - -```bash -$ swift package-collection describe https://www.example.com/bad-packages.json -The collection's signature is invalid. If you would like to continue please rerun command with '--skip-signature-check'. -``` - -Users may continue previewing the collection despite the error or preemptively skip the signature check on a package collection by passing the `--skip-signature-check` flag: - -```bash -$ swift package-collection describe https://www.example.com/packages.json --skip-signature-check -``` - -#### Metadata of a package - -`describe` can also show the metadata of a package included in an imported collection: - -```bash -$ swift package-collection describe [--json] https://github.com/jpsim/yams -Description: A sweet and swifty YAML parser built on LibYAML. -Available Versions: 4.0.0, 3.0.0, ... -Stars: 14 -Readme: https://github.com/jpsim/Yams/blob/master/README.md -Authors: @norio-nomura, @jpsim --------------------------------------------------------------- -Latest Version: 4.0.0 -Package Name: Yams -Modules: Yams, CYaml -Supported Platforms: iOS, macOS, Linux, tvOS, watchOS -Supported Swift Versions: 5.3, 5.2, 5.1, 5.0 -License: MIT -``` - -#### Metadata of a package version - -User may view additional metadata for a package version by passing `--version`: - -```bash -$ swift package-collection describe [--json] --version 4.0.0 https://github.com/jpsim/yams -Package Name: Yams -Version: 4.0.0 -Modules: Yams, CYaml -Supported Platforms: iOS, macOS, Linux, tvOS, watchOS -Supported Swift Versions: 5.3, 5.2, 5.1, 5.0 -License: MIT -``` - -### `list` subcommand - -This subcommand lists all collections that are configured by the user: - -```bash -$ swift package-collection list [--json] -Sample Package Collection - https://example.com/packages.json -... -``` - -The result can optionally be returned as JSON using `--json` for integration into other tools. - -### `refresh` subcommand - -This subcommand refreshes any cached data manually: - -```bash -$ swift package-collection refresh -Refreshed 5 configured package collections. -``` - -SwiftPM will also automatically refresh data under various conditions, but some queries such as search will rely on locally cached data. - -### `remove` subcommand - -This subcommand removes a collection from the user's list of configured collections: - -```bash -$ swift package-collection remove https://www.example.com/packages.json -Removed "Sample Package Collection" from your package collections. -``` - -### `search` subcommand - -This subcommand searches for packages by keywords or module names within imported collections. The result can optionally be returned as JSON using `--json` for -integration into other tools. - -#### String-based search - -The search command does a string-based search when using the `--keywords` option and returns the list of packages that matches the query: - -```bash -$ swift package-collection search [--json] --keywords yaml -https://github.com/jpsim/yams: A sweet and swifty YAML parser built on LibYAML. -... -``` - -#### Module-based search - -The search command does a search for a specific module name when using the `--module` option: - -```bash -$ swift package-collection search [--json] --module yams -Package Name: Yams -Latest Version: 4.0.0 -Description: A sweet and swifty YAML parser built on LibYAML. --------------------------------------------------------------- -... -``` - -## Configuration file - -Configuration that pertains to package collections are stored in the file `~/.swiftpm/config/collections.json`. It keeps track of user's list of configured collections -and preferences such as those set by the `--trust-unsigned` and `--skip-signature-check` flags in the [`package-collection add` command](#add-subcommand). - -This file is managed through SwiftPM commands and users are not expected to edit it by hand. - ---- - -## Publishing package collections - -Package collections can be created and published by anyone. The [swift-package-collection-generator](https://github.com/apple/swift-package-collection-generator) project provides tooling -intended for package collection publishers: -- [`package-collection-generate`](https://github.com/apple/swift-package-collection-generator/tree/main/Sources/PackageCollectionGenerator): Generate a package collection given a list of package URLs -- [`package-collection-sign`](https://github.com/apple/swift-package-collection-generator/tree/main/Sources/PackageCollectionSigner): Sign a package collection -- [`package-collection-validate`](https://github.com/apple/swift-package-collection-generator/tree/main/Sources/PackageCollectionValidator): Perform basic validations on a package collection -- [`package-collection-diff`](https://github.com/apple/swift-package-collection-generator/tree/main/Sources/PackageCollectionDiff): Compare two package collections to see if their contents are different - -### Creating package collections - -All package collections must adhere to the [collection data format](../Sources/PackageCollectionsModel/Formats/v1.md) for SwiftPM to be able to consume them. The recommended way -to create package collections is to use [`package-collection-generate`](https://github.com/apple/swift-package-collection-generator/tree/main/Sources/PackageCollectionGenerator). For custom implementations, the data models are available through the [`PackageCollectionsModel` module](../Sources/PackageCollectionsModel). - -### Package collection signing (optional) - -Package collections can be signed to establish authenticity and protect their integrity. Doing this is optional. Users will be prompted for confirmation before they can add an [unsigned collection](#unsigned-package-collections). - -[`package-collection-sign`](https://github.com/apple/swift-package-collection-generator/tree/main/Sources/PackageCollectionSigner) helps publishers sign their package -collections. To generate a signature one must provide: -- The package collection file to be signed -- A code signing certificate (DER-encoded) -- The certificate's private key (PEM-encoded) -- The certificate's chain in its entirety - -A signed package collection has an extra `signature` object: - -```json -{ - ..., - "signature": { - "signature": "", - "certificate": { - "subject": { - "commonName": "Jane Doe", - ... - }, - "issuer": { - "commonName": "Sample CA", - ... - } - } - } -} -``` - -- The signature string (represented by `""`) is used to verify the contents of the collection file haven't been tampered with since it was signed when SwiftPM user [adds the collection](#signed-package-collections) to their configured list of collections. It includes the certificate's public key and chain. -- `certificate` contains details extracted from the signing certificate. `subject.commonName` should be consistent with the name of the publisher so that it's recognizable by users. The root of the certificate must be [installed and trusted on users' machines](#trusted-root-certificates). - -#### Requirements on signing certificate - -Certificates used for signing package collections must meet the following requirements, which are checked and enforced during signature generation (publishers) and verification (SwiftPM users): -- The timestamp at which signing/verification is done must fall within the signing certificate's validity period. -- The certificate's "Extended Key Usage" extension must include "Code Signing". -- The certificate must use either 256-bit EC (recommended for enhanced security) or 2048-bit RSA key. -- The certificate must not be revoked. The certificate authority must support OCSP, which means the certificate must have the "Certificate Authority Information Access" extension that includes OCSP as a method, specifying the responder's URL. -- The certificate chain is valid and root certificate must be trusted. - -Non-expired, non-revoked Swift Package Collection certificates from [developer.apple.com](https://developer.apple.com) satisfy all of the criteria above. - -##### Trusted root certificates - -With the `package-collection-sign` tool, the root certificate provided as input for signing a collection is automatically trusted. When SwiftPM user tries to add the collection, however, -the root certificate must either be preinstalled with the OS (Apple platforms only) or found in the `~/.swiftpm/config/trust-root-certs` directory (all platforms) or shipped with -the [certificate-pinning configuration](#protecting-package-collections), otherwise the [signature check](#signed-package-collections) will fail. Collection publishers should make the DER-encoded -root certificate(s) that they use downloadable so that users can adjust their setup if needed. - -### Protecting package collections - -[Signing](#package-collection-signing-optional) can provide some degree of protection on package collections and reduce the risks of their contents being modified by malicious actors, but it doesn't -prevent the following attack vectors: -- **Signature stripping**: This involves attackers removing signature from a signed collection, causing it to be downloaded as an [unsigned collection](#unsigned-package-collections) and bypassing signature check. In this case, publishers should make it known that the collection is signed, and SwiftPM users should abort the `add` operation when the "unsigned" warning appears on a supposedly signed collection. -- **Signature replacement**: Attackers may modify a collection then re-sign it using a different certificate, either pretend to be the same entity or as some other entity, and SwiftPM will accept it as long as the [signature is valid](#signed-package-collections). - -To defend against these attacks, SwiftPM has certificate-pinning configuration that allows collection publishers to: -- Require signature check on their collections — this defends against "signature stripping". -- Restrict what certificate can be used for signing — this defends against "signature replacement". - -The process for collection publishers to define their certificate-pinning configuration is as follows: -1. Edit [`PackageCollectionSourceCertificatePolicy`](../Sources/PackageCollections/PackageCollections+CertificatePolicy.swift) and add an entry to the `defaultSourceCertPolicies` dictionary: - -```swift -private static let defaultSourceCertPolicies: [String: CertificatePolicyConfig] = [ - // The key should be the "host" component of the package collection URL. - // This would require all package collections hosted on this domain to be signed. - "www.example.com": CertificatePolicyConfig( - // The signing certificate must have this subject user ID - certPolicyKey: CertificatePolicyKey.default(subjectUserID: "exampleUserID"), - /* - To compute base64-encoded string of a certificate: - let certificateURL = URL(fileURLWithPath: ) - let certificateData = try Data(contentsOf: certificateURL) - let base64EncoodedCertificate = certificateData.base64EncodedString() - */ - base64EncodedRootCerts: [""] - ) -] -``` - -2. Open a pull request for review. The requestor must be able to provide proof of their identity and ownership on the domain: - - The requestor must provide the actual certificate files (DER-encoded). The SwiftPM team will verify that the certificate chain is valid and the values provided in the PR are correct. - - The requestor must add a TXT record referencing the pull request. The SwiftPM team will run `dig -t txt ` to verify. This would act as proof of domain ownership. -3. After the changes are accepted, they will take effect in the next SwiftPM release. - -Since certificate-pinning configuration is associated with web domains, it can only be applied to signed collections hosted on the web (i.e., URL begins with `https://`) and does -not cover those found on local file system (i.e., URL begins with `file://`). diff --git a/Documentation/PackageDescription.md b/Documentation/PackageDescription.md deleted file mode 100644 index 7d2b6aa1260..00000000000 --- a/Documentation/PackageDescription.md +++ /dev/null @@ -1,1125 +0,0 @@ -# Package - -`class Package` - -The configuration of a Swift package. - -Pass configuration options as parameters to your package's initializer -statement to provide the name of the package, its targets, products, -dependencies, and other configuration options. - -By convention, the properties of a `Package` are defined in a single nested -initializer statement, and not modified after initialization. The following package -manifest shows the initialization of a simple package object for the MyLibrary -Swift package: - -```swift -// swift-tools-version:5.1 -import PackageDescription - -let package = Package( - name: "MyLibrary", - platforms: [ - .macOS(.v10_15), - ], - products: [ - .library(name: "MyLibrary", targets: ["MyLibrary"]), - ], - dependencies: [ - .package(url: "https://url/of/another/package/named/Utility", from: "1.0.0"), - ], - targets: [ - .target(name: "MyLibrary", dependencies: ["Utility"]), - .testTarget(name: "MyLibraryTests", dependencies: ["MyLibrary"]), - ] -) -``` - -## Methods - -
-Package(
-    name: String,
-    defaultLocalization: [LanguageTag]? = nil.
-    platforms: [SupportedPlatform]? = nil,
-    products: [Product] = [],
-    dependencies: [Package.Dependency] = [],
-    targets: [Target] = [],
-    swiftLanguageVersions: [SwiftVersion]? = nil,
-    cLanguageStandard: CLanguageStandard? = nil,
-    cxxLanguageStandard: CXXLanguageStandard? = nil
-)
-
- -### About the Swift Tools Version - -A `Package.swift` manifest file must begin with the string -`// swift-tools-version:` followed by a version number specifier. -The following code listing shows a few examples of valid declarations -of the Swift tools version: - - // swift-tools-version:3.0.2 - // swift-tools-version:3.1 - // swift-tools-version:4.0 - // swift-tools-version:5.0 - // swift-tools-version:5.1 - // swift-tools-version:5.2 - // swift-tools-version:5.3 - -The Swift tools version declares the version of the `PackageDescription` -library, the minimum version of the Swift tools and Swift language -compatibility version to process the manifest, and the minimum version of the -Swift tools that are needed to use the Swift package. Each version of Swift -can introduce updates to the `PackageDescription` framework, but the previous -API version will continue to be available to packages which declare a prior -tools version. This behavior lets you take advantage of new releases of -Swift, the Swift tools, and the `PackageDescription` library, without having -to update your package's manifest or losing access to existing packages. - -# SupportedPlatform - -`struct SupportedPlatform` - -A platform that the Swift package supports. - -By default, the Swift Package Manager assigns a predefined minimum deployment -version for each supported platforms unless you configure supported platforms using the `platforms` -API. This predefined deployment version is the oldest deployment target -version that the installed SDK supports for a given platform. One exception -to this rule is macOS, for which the minimum deployment target version -starts from 10.10. Packages can choose to configure the minimum deployment -target version for a platform by using the APIs defined in this struct. The -Swift Package Manager emits appropriate errors when an invalid value is -provided for supported platforms, such as an empty array, multiple declarations -for the same platform, or an invalid version specification. - -The Swift Package Manager will emit an error if a dependency is not -compatible with the top-level package's deployment version. The deployment -target of a package's dependencies must be lower than or equal to the top-level package's -deployment target version for a particular platform. - -## Methods - -```swift -/// Configure the minimum deployment target version for the macOS platform. -/// -/// - Since: First available in PackageDescription 5.0 -/// -/// - Parameter version: The minimum deployment target that the package supports. -static func macOS(_ version: SupportedPlatform.MacOSVersion) -> SupportedPlatform - -/// Configure the minimum deployment target version for the macOS platform -/// using a version string. -/// -/// The version string must be a series of two or three dot-separated integers, such as `10.10` or `10.10.1`. -/// -/// - Since: First available in PackageDescription 5.0 -/// -/// - Parameter versionString: The minimum deployment target as a string representation of two or three dot-separated integers, such as `10.10.1`. -static func macOS(_ versionString: String) -> SupportedPlatform - -/// Configure the minimum deployment target version for the iOS platform. -/// -/// - Since: First available in PackageDescription 5.0 -/// -/// - Parameter version: The minimum deployment target that the package supports. -static func iOS(_ version: SupportedPlatform.IOSVersion) -> SupportedPlatform - -/// Configure the minimum deployment target version for the iOS platform -/// using a custom version string. -/// -/// The version string must be a series of two or three dot-separated integers, such as `8.0` or `8.0.1`. -/// -/// - Since: First available in PackageDescription 5.0 -/// -/// - Parameter versionString: The minimum deployment target as a string representation of two or three dot-separated integers, such as `8.0.1`. -static func iOS(_ versionString: String) -> SupportedPlatform - -/// Configure the minimum deployment target version for the tvOS platform. -/// -/// - Since: First available in PackageDescription 5.0 -/// -/// - Parameter version: The minimum deployment target that the package supports. -static func tvOS(_ version: SupportedPlatform.TVOSVersion) -> SupportedPlatform - -/// Configure the minimum deployment target version for the tvOS platform -/// using a custom version string. -/// -/// The version string must be a series of two or three dot-separated integers,such as `9.0` or `9.0.1`. -/// -/// - Since: First available in PackageDescription 5.0 -/// -/// - Parameter versionString: The minimum deployment target as a string representation of two or three dot-separated integers, such as `9.0.1`. -static func tvOS(_ versionString: String) -> SupportedPlatform - -/// Configure the minimum deployment target version for the watchOS platform. -/// -/// - Since: First available in PackageDescription 5.0 -/// -/// - Parameter version: The minimum deployment target that the package supports. -static func watchOS(_ version: SupportedPlatform.WatchOSVersion) -> SupportedPlatform - -/// Configure the minimum deployment target version for the watchOS platform -/// using a custom version string. -/// -/// The version string must be a series of two or three dot-separated integers, such as `2.0` or `2.0.1`. -/// -/// - Since: First available in PackageDescription 5.0 -/// -/// - Parameter versionString: The minimum deployment target as a string representation of two or three dot-separated integers, such as `2.0.1`. -static func watchOS(_ versionString: String) -> SupportedPlatform -``` - -# Product - -`class Product` - -The object that defines a package product. - -A package product defines an externally visible build artifact that's -available to clients of a package. The product is assembled from the build -artifacts of one or more of the package's targets. - -A package product can be one of two types: - -1. **Library**. Use a library product to vend library targets. This makes a target's public APIs -available to clients that integrate the Swift package. -2. **Executable**. Use an executable product to vend an executable target. -Use this only if you want to make the executable available to clients. - -The following example shows a package manifest for a library called "Paper" -that defines multiple products: - -```swift -let package = Package( - name: "Paper", - products: [ - .executable(name: "tool", targets: ["tool"]), - .library(name: "Paper", targets: ["Paper"]), - .library(name: "PaperStatic", type: .static, targets: ["Paper"]), - .library(name: "PaperDynamic", type: .dynamic, targets: ["Paper"]), - ], - dependencies: [ - .package(url: "http://example.com/ExamplePackage/ExamplePackage", from: "1.2.3"), - .package(url: "http://some/other/lib", .exact("1.2.3")), - ], - targets: [ - .target( - name: "tool", - dependencies: [ - "Paper", - "ExamplePackage" - ]), - .target( - name: "Paper", - dependencies: [ - "Basic", - .target(name: "Utility"), - .product(name: "AnotherExamplePackage"), - ]) - ] -) -``` - -## Methods - -```swift -/// Create a library product to allow clients that declare a dependency on this package -/// to use the package's functionality. -/// -/// A library's product can either be statically or dynamically linked. -/// If possible, don't declare the type of library explicitly to let -/// the Swift Package Manager choose between static or dynamic linking based -/// on the preference of the package's consumer. -/// -/// - Parameters: -/// - name: The name of the library product. -/// - type: The optional type of the library that is used to determine how to link to the library. -/// Leave this parameter unspecified to let to let the Swift Package Manager choose between static or dynamic linking (recommended). -/// If you do not support both linkage types, use `.static` or `.dynamic` for this parameter. -/// - targets: The targets that are bundled into a library product. -static func library(name: String, type: Product.Library.LibraryType? = nil, targets: [String]) -> Product - -/// Create an executable package product that clients can run. -/// -/// - Parameters: -/// - name: The name of the executable product. -/// - targets: The targets to bundle into an executable product. -static func executable(name: String, targets: [String]) -> Product -``` - -# Package Dependency - -`class Package.Dependency` - -A package dependency of a Swift package. - -A package dependency consists of a Git URL to the source of the package, -and a requirement for the version of the package. - -The Swift Package Manager performs a process called *dependency resolution* to -figure out the exact version of the package dependencies that an app or other -Swift package can use. The `Package.resolved` file records the results of the -dependency resolution and lives in the top-level directory of a Swift package. -If you add the Swift package as a package dependency to an app for an Apple platform, -you can find the `Package.resolved` file inside your `.xcodeproj` or `.xcworkspace`. - -## Methods - -```swift -/// Create a package dependency that uses the version requirement, starting with the given minimum version, -/// going up to the next major version. -/// -/// This is the recommended way to specify a remote package dependency. -/// It allows you to specify the minimum version you require, allows updates that include bug fixes -/// and backward-compatible feature updates, but requires you to explicitly update to a new major version of the dependency. -/// This approach provides the maximum flexibility on which version to use, -/// while making sure you don't update to a version with breaking changes, -/// and helps to prevent conflicts in your dependency graph. -/// -/// The following example allows the Swift Package Manager to select a version -/// like a `1.2.3`, `1.2.4`, or `1.3.0`, but not `2.0.0`. -/// -/// .package(url: "https://example.com/example-package.git", from: "1.2.3"), -/// -/// - Parameters: -/// - name: The name of the package, or nil to deduce it from the URL. -/// - url: The valid Git URL of the package. -/// - version: The minimum version requirement. -static func package(url: String, from version: Version) -> Package.Dependency - -/// Add a remote package dependency given a version requirement. -/// -/// - Parameters: -/// - name: The name of the package, or nil to deduce it from the URL. -/// - url: The valid Git URL of the package. -/// - requirement: A dependency requirement. See static methods on `Package.Dependency.Requirement` for available options. -static func package(url: String, _ requirement: Package.Dependency.Requirement) -> Package.Dependency - -/// Adds a remote package dependency given a branch requirement. -/// -/// .package(url: "https://example.com/example-package.git", branch: "main"), -/// -/// - Parameters: -/// - name: The name of the package, or nil to deduce it from the URL. -/// - url: The valid Git URL of the package. -/// - branch: A dependency requirement. See static methods on `Package.Dependency.Requirement` for available options. -static func package(name: String? = nil, url: String, branch: String) -> Package.Dependency - -/// Adds a remote package dependency given a revision requirement. -/// -/// .package(url: "https://example.com/example-package.git", revision: "aa681bd6c61e22df0fd808044a886fc4a7ed3a65"), -/// -/// - Parameters: -/// - name: The name of the package, or nil to deduce it from the URL. -/// - url: The valid Git URL of the package. -/// - revision: A dependency requirement. See static methods on `Package.Dependency.Requirement` for available options. -static func package(name: String? = nil, url: String, revision: String) -> Package.Dependency - -/// Add a package dependency starting with a specific minimum version, up to -/// but not including a specified maximum version. -/// -/// The following example allows the Swift Package Manager to pick -/// versions `1.2.3`, `1.2.4`, `1.2.5`, but not `1.2.6`. -/// -/// .package(url: "https://example.com/example-package.git", "1.2.3"..<"1.2.6"), -/// -/// - Parameters: -/// - name: The name of the package, or nil to deduce it from the URL. -/// - url: The valid Git URL of the package. -/// - range: The custom version range requirement. -static func package(url: String, _ range: Range) -> Package.Dependency - -/// Add a package dependency starting with a specific minimum version, going -/// up to and including a specific maximum version. -/// -/// The following example allows the Swift Package Manager to pick -/// versions 1.2.3, 1.2.4, 1.2.5, as well as 1.2.6. -/// -/// .package(url: "https://example.com/example-package.git", "1.2.3"..."1.2.6"), -/// -/// - Parameters: -/// - name: The name of the package, or nil to deduce it from the URL. -/// - url: The valid Git URL of the package. -/// - range: The closed version range requirement. -static func package(url: String, _ range: ClosedRange) -> Package.Dependency - -/// Add a dependency to a local package on the filesystem. -/// -/// The Swift Package Manager uses the package dependency as-is -/// and does not perform any source control access. Local package dependencies -/// are especially useful during development of a new package or when working -/// on multiple tightly coupled packages. -/// -/// - Parameter path: The path of the package. -static func package(path: String) -> Package.Dependency -``` - -# Package Dependency Requirement - -`enum Package.Dependency.Requirement` - -An enum that represents the requirement for a package dependency. - -The dependency requirement can be defined as one of three different version requirements: - -**A version-based requirement.** - -Decide whether your project accepts updates to a package dependency up -to the next major version or up to the next minor version. To be more -restrictive, select a specific version range or an exact version. -Major versions tend to have more significant changes than minor -versions, and may require you to modify your code when they update. -The version rule requires Swift packages to conform to semantic -versioning. To learn more about the semantic versioning standard, -visit [semver.org](https://semver.org). - -Selecting the version requirement is the recommended way to add a package dependency. It allows you to create a balance between restricting changes and obtaining improvements and features. - -**A branch-based requirement** - -Select the name of the branch for your package dependency to follow. -Use branch-based dependencies when you're developing multiple packages -in tandem or when you don't want to publish versions of your package dependencies. - -Note that packages which use branch-based dependency requirements -can't be added as dependencies to packages that use version-based dependency -requirements; you should remove branch-based dependency requirements -before publishing a version of your package. - -**A commit-based requirement** - -Select the commit hash for your package dependency to follow. -Choosing this option isn't recommended, and should be limited to -exceptional cases. While pinning your package dependency to a specific -commit ensures that the package dependency doesn't change and your -code remains stable, you don't receive any updates at all. If you worry about -the stability of a remote package, consider one of the more -restrictive options of the version-based requirement. - -Note that packages which use commit-based dependency requirements -can't be added as dependencies to packages that use version-based -dependency requirements; you should remove commit-based dependency -requirements before publishing a version of your package. - -## Methods - -```swift -/// Returns a requirement for the given exact version. -/// -/// Specifying exact version requirements are not recommended as -/// they can cause conflicts in your dependency graph when multiple other packages depend on a package. -/// As Swift packages follow the semantic versioning convention, -/// think about specifying a version range instead. -/// -/// The following example defines a version requirement that requires version 1.2.3 of a package. -/// -/// .exact("1.2.3") -/// -/// - Parameters: -/// - version: The exact version of the dependency for this requirement. -static func exact(_ version: Version) -> Package.Dependency.Requirement - -/// Returns a requirement for a source control revision such as the hash of a commit. -/// -/// Note that packages that use commit-based dependency requirements -/// can't be depended upon by packages that use version-based dependency -/// requirements; you should remove commit-based dependency requirements -/// before publishing a version of your package. -/// -/// The following example defines a version requirement for a specific commit hash. -/// -/// .revision("e74b07278b926c9ec6f9643455ea00d1ce04a021") -/// -/// - Parameters: -/// - ref: The Git revision, usually a commit hash. -static func revision(_ ref: String) -> Package.Dependency.Requirement - -/// Returns a requirement for a source control branch. -/// -/// Note that packages that use branch-based dependency requirements -/// can't be depended upon by packages that use version-based dependency -/// requirements; you should remove branch-based dependency requirements -/// before publishing a version of your package. -/// -/// The following example defines a version requirement that accepts any -/// change in the develop branch. -/// -/// .branch("develop") -/// -/// - Parameters: -/// - name: The name of the branch. -static func branch(_ name: String) -> Package.Dependency.Requirement - -/// Returns a requirement for a version range, starting at the given minimum -/// version and going up to the next major version. This is the recommended version requirement. -/// -/// - Parameters: -/// - version: The minimum version for the version range. -static func upToNextMajor(from version: Version) -> Package.Dependency.Requirement - -/// Returns a requirement for a version range, starting at the given minimum -/// version and going up to the next minor version. -/// -/// - Parameters: -/// - version: The minimum version for the version range. -static func upToNextMinor(from version: Version) -> Package.Dependency.Requirement -``` - -# Version - -`struct Version` - -A version according to the semantic versioning specification. - -A package version is a three period-separated integer, for example `1.0.0`. It must conform to the semantic versioning standard in order to ensure -that your package behaves in a predictable manner once developers update their -package dependency to a newer version. To achieve predictability, the semantic versioning specification proposes a set of rules and -requirements that dictate how version numbers are assigned and incremented. To learn more about the semantic versioning specification, visit -[semver.org](https://semver.org). - -**The Major Version** - -The first digit of a version, or *major version*, signifies breaking changes to the API that require -updates to existing clients. For example, the semantic versioning specification -considers renaming an existing type, removing a method, or changing a method's signature -breaking changes. This also includes any backward-incompatible bug fixes or -behavioral changes of the existing API. - -**The Minor Version** - -Update the second digit of a version, or *minor version*, if you add functionality in a backward-compatible manner. -For example, the semantic versioning specification considers adding a new method -or type without changing any other API to be backward-compatible. - -**The Patch Version** - -Increase the third digit of a version, or *patch version*, if you are making a backward-compatible bug fix. -This allows clients to benefit from bugfixes to your package without incurring -any maintenance burden. - -# Target - -`class Target` - -A target, the basic building block of a Swift package. - -Each target contains a set of source files that are compiled into a module or test suite. -You can vend targets to other packages by defining products that include the targets. - -A target may depend on other targets within the same package and on products vended by the package's dependencies. - -## Methods - -```swift -/// Creates a regular target. -/// -/// A target can contain either Swift or C-family source files, but not both. It contains code that is built as -/// a regular module that can be included in a library or executable product, but that cannot itself be used as -/// the main target of an executable product. -/// -/// - Parameters: -/// - name: The name of the target. -/// - dependencies: The dependencies of the target. A dependency can be another target in the package or a product from a package dependency. -/// - path: The custom path for the target. By default, the Swift Package Manager requires a target's sources to reside at predefined search paths; -/// for example, `[PackageRoot]/Sources/[TargetName]`. -/// Don't escape the package root; for example, values like `../Foo` or `/Foo` are invalid. -/// - exclude: A list of paths to files or directories that the Swift Package Manager shouldn't consider to be source or resource files. -/// A path is relative to the target's directory. -/// This parameter has precedence over the `sources` parameter. -/// - sources: An explicit list of source files. If you provide a path to a directory, -/// the Swift Package Manager searches for valid source files recursively. -/// - resources: An explicit list of resources files. -/// - publicHeadersPath: The directory containing public headers of a C-family library target. -/// - cSettings: The C settings for this target. -/// - cxxSettings: The C++ settings for this target. -/// - swiftSettings: The Swift settings for this target. -/// - linkerSettings: The linker settings for this target. -static func target( - name: String, - dependencies: [Target.Dependency] = [], - path: String? = nil, - exclude: [String] = [], - sources: [String]? = nil, - resources: [Resource]? = nil, - publicHeadersPath: String? = nil, - cSettings: [CSetting]? = nil, - cxxSettings: [CXXSetting]? = nil, - swiftSettings: [SwiftSetting]? = nil, - linkerSettings: [LinkerSetting]? = nil -) -> Target - -/// Creates an executable target. -/// -/// An executable target can contain either Swift or C-family source files, but not both. It contains code that -/// is built as an executable module that can be used as the main target of an executable product. The target -/// is expected to either have a source file named `main.swift`, `main.m`, `main.c`, or `main.cpp`, or a source -/// file that contains the `@main` keyword. -/// -/// - Parameters: -/// - name: The name of the target. -/// - dependencies: The dependencies of the target. A dependency can be another target in the package or a product from a package dependency. -/// - path: The custom path for the target. By default, the Swift Package Manager requires a target's sources to reside at predefined search paths; -/// for example, `[PackageRoot]/Sources/[TargetName]`. -/// Don't escape the package root; for example, values like `../Foo` or `/Foo` are invalid. -/// - exclude: A list of paths to files or directories that the Swift Package Manager shouldn't consider to be source or resource files. -/// A path is relative to the target's directory. -/// This parameter has precedence over the `sources` parameter. -/// - sources: An explicit list of source files. If you provide a path to a directory, -/// the Swift Package Manager searches for valid source files recursively. -/// - resources: An explicit list of resources files. -/// - publicHeadersPath: The directory containing public headers of a C-family library target. -/// - cSettings: The C settings for this target. -/// - cxxSettings: The C++ settings for this target. -/// - swiftSettings: The Swift settings for this target. -/// - linkerSettings: The linker settings for this target. -static func executableTarget( - name: String, - dependencies: [Target.Dependency] = [], - path: String? = nil, - exclude: [String] = [], - sources: [String]? = nil, - resources: [Resource]? = nil, - publicHeadersPath: String? = nil, - cSettings: [CSetting]? = nil, - cxxSettings: [CXXSetting]? = nil, - swiftSettings: [SwiftSetting]? = nil, - linkerSettings: [LinkerSetting]? = nil -) -> Target - -/// Creates a test target. -/// -/// Write test targets using the XCTest testing framework. -/// Test targets generally declare a dependency on the targets they test. -/// -/// - Parameters: -/// - name: The name of the target. -/// - dependencies: The dependencies of the target. A dependency can be another target in the package or a product from a package dependency. -/// - path: The custom path for the target. By default, the Swift Package Manager requires a target's sources to reside at predefined search paths; -/// for example, `[PackageRoot]/Sources/[TargetName]`. -/// Don't escape the package root; for example, values like `../Foo` or `/Foo` are invalid. -/// - exclude: A list of paths to files or directories that the Swift Package Manager shouldn't consider to be source or resource files. -/// A path is relative to the target's directory. -/// This parameter has precedence over the `sources` parameter. -/// - sources: An explicit list of source files. If you provide a path to a directory, -/// the Swift Package Manager searches for valid source files recursively. -/// - resources: An explicit list of resources files. -/// - cSettings: The C settings for this target. -/// - cxxSettings: The C++ settings for this target. -/// - swiftSettings: The Swift settings for this target. -/// - linkerSettings: The linker settings for this target. -static func testTarget( - name: String, - dependencies: [Target.Dependency] = [], - path: String? = nil, - exclude: [String] = [], - sources: [String]? = nil, - resources: [Resource]? = nil, - cSettings: [CSetting]? = nil, - cxxSettings: [CXXSetting]? = nil, - swiftSettings: [SwiftSetting]? = nil, - linkerSettings: [LinkerSetting]? = nil -) -> Target - -/// Creates a system library target. -/// -/// Use system library targets to adapt a library installed on the system to work with Swift packages. -/// Such libraries are generally installed by system package managers (such as Homebrew and apt-get) -/// and exposed to Swift packages by providing a `modulemap` file along with other metadata such as the library's `pkgConfig` name. -/// -/// - Parameters: -/// - name: The name of the target. -/// - path: The custom path for the target. By default, the Swift Package Manager requires a target's sources to reside at predefined search paths; -/// for example, `[PackageRoot]/Sources/[TargetName]`. -/// Don't escape the package root; for example, values like `../Foo` or `/Foo` are invalid. -/// - pkgConfig: The name of the `pkg-config` file for this system library. -/// - providers: The providers for this system library. -static func systemLibrary( - name: String, - path: String? = nil, - pkgConfig: String? = nil, - providers: [SystemPackageProvider]? = nil -) -> Target - -/// Creates a binary target that references a remote artifact. -/// -/// A binary target provides the url to a pre-built binary artifact for the target. Currently only supports -/// artifacts for Apple platforms. -/// -/// - Parameters: -/// - name: The name of the target. -/// - url: The URL to the binary artifact. This URL must point to an archive file -/// that contains a binary artifact in its root directory. -/// - checksum: The checksum of the archive file that contains the binary artifact. -/// -/// Binary targets are only available on Apple platforms. -static func binaryTarget( - name: String, - url: String, - checksum: String -) -> Target - -/// Creates a binary target that references an artifact on disk. -/// -/// A binary target provides the path to a pre-built binary artifact for the target. -/// The Swift Package Manager only supports binary targets for Apple platforms. -/// -/// - Parameters: -/// - name: The name of the target. -/// - path: The path to the binary artifact. This path can point directly to a binary artifact -/// or to an archive file that contains the binary artifact at its root. -/// -/// Binary targets are only available on Apple platforms. -static func binaryTarget( - name: String, - path: String -) -> Target - -``` - -# Target Dependency - -`class Target.Dependency` - -The different types of a target's dependency on another entity. - -## Methods - -```swift -/// Creates a dependency on a target in the same package. -/// -/// - parameters: -/// - name: The name of the target. -/// - condition: A condition that limits the application of the target dependency. For example, only apply a -/// dependency for a specific platform. -static func target( - name: String, - condition: TargetDependencyCondition? = nil -) -> Target.Dependency - -/// Creates a target dependency on a product from a package dependency. -/// -/// - parameters: -/// - name: The name of the product. -/// - package: The name of the package. -/// - condition: A condition that limits the application of the target dependency. For example, only apply a -/// dependency for a specific platform. -static func product( - name: String, - package: String, - condition: TargetDependencyCondition? = nil -) -> Target.Dependency - -/// Creates a by-name dependency that resolves to either a target or a product but after the Swift Package Manager -/// has loaded the package graph. -/// -/// - parameters: -/// - name: The name of the dependency, either a target or a product. -/// - condition: A condition that limits the application of the target dependency. For example, only apply a -/// dependency for a specific platform. -static func byName( - name: String - condition: TargetDependencyCondition? = nil -) -> Target.Dependency -``` - -# Target Dependency Condition - -`class TargetDependencyCondition` - -A condition that limits the application of a target's dependency. - -## Methods - -```swift -/// Creates a target dependency condition. -/// -/// - Parameters: -/// - platforms: The applicable platforms for this target dependency condition. -static func when(platforms: [Platform]? = nil) -> TargetDependencyCondition -``` - -# Resource - -`struct Resource` - -A resource to bundle with the Swift package. - -If a Swift package declares a Swift tools version of 5.3 or later, it can include resource files. - -Similar to source code, the Swift Package Manager scopes resources to a target, so you must put them -into the folder that corresponds to the target they belong to. -For example, any resources for the `MyLibrary` target must reside in `Sources/MyLibrary`. - -Use subdirectories to organize your resource files in a way that simplifies file identification and management. -For example, put all resource files into a directory named `Resources`, -so they reside at `Sources/MyLibrary/Resources`. - -By default, the Swift Package Manager handles common resources types for Apple platforms automatically. -For example, you don’t need to declare XIB files, storyboards, Core Data file types, and asset catalogs -as resources in your package manifest. - -However, you must explicitly declare other file types—for example image files—as resources -using the `process(_:localization:)` or `copy(_:)` rules. - -Alternatively, exclude resource files from a target -by passing them to the target initializer’s `exclude` parameter. - -## Methods - -```swift -/// Applies a platform-specific rule to the resource at the given path. -/// -/// Use the `process` rule to process resources at the given path -/// according to the platform it builds the target for. For example, the -/// Swift Package Manager may optimize image files for platforms that -/// support such optimizations. If no optimization is available for a file -/// type, the Swift Package Manager copies the file. -/// -/// If the given path represents a directory, the Swift Package Manager -/// applies the process rule recursively to each file in the directory. -/// -/// If possible use this rule instead of `copy(_:)`. -/// -/// - Parameters: -/// - path: The path for a resource. -/// - localization: The explicit localization type for the resource. -static func process( - _ path: String, - localization: Localization? = nil -) -> Resource - -/// Applies the copy rule to a resource at the given path. -/// -/// If possible, use `process(_:localization:)`` and automatically apply optimizations -/// to resources. -/// -/// If your resources must remain untouched or must retain a specific folder structure, -/// use the `copy` rule. It copies resources at the given path, as is, to the top level -/// in the package’s resource bundle. If the given path represents a directory, Xcode preserves its structure. -/// -/// - Parameters: -/// - path: The path for a resource. -static func copy( - _ path: String -) -> Resource -``` - -# Localization - -`struct Resource.Localization` - -Defines the explicit type of localization for resources. - -## Cases - -```swift -/// A constant that represents default internationalization. -case `default` - -/// A constant that represents base internationalization. -case base -``` - -# LanguageTag - -`struct LanguageTag` - -A wrapper around an IETF language tag. - -To learn more about the IETF worldwide standard for language tags, -see [RFC5646](https://tools.ietf.org/html/rfc5646). - -## Methods - -```swift -/// Creates a language tag from its IETF string representation. -init(_ tag: String) -``` - -# CSetting - -`struct CSetting` - -A C-language build setting. - -## Methods - -```swift -/// Provides a header search path relative to the target's directory. -/// -/// Use this setting to add a search path for headers within your target. -/// You can't use absolute paths and you can't use this setting to provide -/// headers that are visible to other targets. -/// -/// The path must be a directory inside the package. -/// -/// - Since: First available in PackageDescription 5.0 -/// -/// - Parameters: -/// - path: The path of the directory that contains the headers. The path is relative to the target's directory. -/// - condition: A condition that restricts the application of the build setting. -static func headerSearchPath(_ path: String, _ condition: BuildSettingCondition? = nil) -> CSetting - -/// Defines a value for a macro. -/// -/// If you don't specify a value, the macro's default value is 1. -/// -/// - Since: First available in PackageDescription 5.0 -/// -/// - Parameters: -/// - name: The name of the macro. -/// - value: The value of the macro. -/// - condition: A condition that restricts the application of the build setting. -static func define(_ name: String, to value: String? = nil, _ condition: BuildSettingCondition? = nil) -> CSetting - -/// Sets unsafe flags to pass arbitrary command-line flags to the corresponding build tool. -/// -/// As the usage of the word "unsafe" implies, the Swift Package Manager -/// can't safely determine if the build flags have any negative -/// side effect on the build since certain flags can change the behavior of -/// how it performs a build. -/// -/// As some build flags can be exploited for unsupported or malicious -/// behavior, the use of unsafe flags make the products containing this -/// target ineligible for use by other packages. -/// -/// - Since: First available in PackageDescription 5.0 -/// -/// - Parameters: -/// - flags: The unsafe flags to set. -/// - condition: A condition that restricts the application of the build setting. -static func unsafeFlags(_ flags: [String], _ condition: BuildSettingCondition? = nil) -> CSetting -``` - -# CXXSetting - -`struct CXXSetting` - -A CXX-language build setting. - -## Methods - -```swift -/// Provides a header search path relative to the target's directory. -/// -/// Use this setting to add a search path for headers within your target. -/// You can't use absolute paths and you can't use this setting to provide -/// headers that are visible to other targets. -/// -/// The path must be a directory inside the package. -/// -/// - Since: First available in PackageDescription 5.0 -/// -/// - Parameters: -/// - path: The path of the directory that contains the headers. The path is relative to the target's directory. -/// - condition: A condition that restricts the application of the build setting. -static func headerSearchPath(_ path: String, _ condition: BuildSettingCondition? = nil) -> CXXSetting - -/// Defines a value for a macro. -/// -/// If you don't specify a value, the macro's default value is 1. -/// -/// - Since: First available in PackageDescription 5.0 -/// -/// - Parameters: -/// - name: The name of the macro. -/// - value: The value of the macro. -/// - condition: A condition that restricts the application of the build setting. -static func define(_ name: String, to value: String? = nil, _ condition: BuildSettingCondition? = nil) -> CXXSetting - -/// Sets unsafe flags to pass arbitrary command-line flags to the corresponding build tool. -/// -/// As the usage of the word "unsafe" implies, the Swift Package Manager -/// can't safely determine if the build flags have any negative -/// side effect on the build since certain flags can change the behavior of -/// how a build is performed. -/// -/// As some build flags can be exploited for unsupported or malicious -/// behavior, a product can't be used as a dependency in another package if one of its targets uses unsafe flags. -/// -/// - Since: First available in PackageDescription 5.0 -/// -/// - Parameters: -/// - flags: The unsafe flags to set. -/// - condition: A condition that restricts the application of the build setting. -static func unsafeFlags(_ flags: [String], _ condition: BuildSettingCondition? = nil) -> CXXSetting -``` - -# SwiftSetting - -`struct SwiftSetting` - -A Swift language build setting. - -## Methods - -```swift -/// Defines a compilation condition. -/// -/// Use compilation conditions to only compile statements if a certain condition is true. -/// For example, the Swift compiler will only compile the -/// statements inside the `#if` block when `ENABLE_SOMETHING` is defined: -/// -/// #if ENABLE_SOMETHING -/// ... -/// #endif -/// -/// Unlike macros in C/C++, compilation conditions don't have an -/// associated value. -/// -/// - Since: First available in PackageDescription 5.0 -/// -/// - Parameters: -/// - name: The name of the macro. -/// - condition: A condition that restricts the application of the build setting. -static func define(_ name: String, _ condition: BuildSettingCondition? = nil) -> SwiftSetting - -/// Sets unsafe flags to pass arbitrary command-line flags to the corresponding build tool. -/// -/// As the usage of the word "unsafe" implies, the Swift Package Manager -/// can't safely determine if the build flags have any negative -/// side effect on the build since certain flags can change the behavior of -/// how a build is performed. -/// -/// As some build flags can be exploited for unsupported or malicious -/// behavior, a product can't be used as a dependency in another package if one of its targets uses unsafe flags. -/// -/// - Since: First available in PackageDescription 5.0 -/// -/// - Parameters: -/// - flags: The unsafe flags to set. -/// - condition: A condition that restricts the application of the build setting.. -static func unsafeFlags(_ flags: [String], _ condition: BuildSettingCondition? = nil) -> SwiftSetting -``` - -# LinkerSetting - -`struct LinkerSetting` - -A linker build setting. - -## Methods - -```swift -/// Declares linkage to a system library. -/// -/// This setting is most useful when the library can't be linked -/// automatically, such as C++ based libraries and non-modular -/// libraries. -/// -/// - Since: First available in PackageDescription 5.0 -/// -/// - Parameters: -/// - library: The library name. -/// - condition: A condition that restricts the application of the build setting. -static func linkedLibrary(_ library: String, _ condition: BuildSettingCondition? = nil) -> LinkerSetting - -/// Declares linkage to a system framework. -/// -/// This setting is most useful when the framework can't be linked -/// automatically, such as C++ based frameworks and non-modular -/// frameworks. -/// -/// - Since: First available in PackageDescription 5.0 -/// -/// - Parameters: -/// - framework: The framework name. -/// - condition: A condition that restricts the application of the build setting. -static func linkedFramework(_ framework: String, _ condition: BuildSettingCondition? = nil) -> LinkerSetting - -/// Sets unsafe flags to pass arbitrary command-line flags to the corresponding build tool. -/// -/// As the usage of the word "unsafe" implies, the Swift Package Manager -/// can't safely determine if the build flags have any negative -/// side effect on the build since certain flags can change the behavior of -/// how a build is performed. -/// -/// As some build flags can be exploited for unsupported or malicious -/// behavior, a product can't be used as a dependency in another package if one of its targets uses unsafe flags. -/// -/// - Since: First available in PackageDescription 5.0 -/// -/// - Parameters: -/// - flags: The unsafe flags to set. -/// - condition: A condition that restricts the application of the build setting. -static func unsafeFlags(_ flags: [String], _ condition: BuildSettingCondition? = nil) -> LinkerSetting -``` - -# SwiftVersion - -`enum SwiftVersion` - -The version of the Swift language to use for compiling Swift sources in the package. - -```swift -enum SwiftVersion { - case v3 - case v4 - case v4_2 - case v5 - - /// A user-defined value for the Swift version. - /// - /// The value is passed as-is to the Swift compiler's `-swift-version` flag. - case version(String) -} -``` - -# CLanguageStandard - -`enum CLanguageStandard` - -The supported C language standard to use for compiling C sources in the package. - -```swift -enum CLanguageStandard { - case c89 - case c90 - case c99 - case c11 - case c17 - case c18 - case c2x - case gnu89 - case gnu90 - case gnu99 - case gnu11 - case gnu17 - case gnu18 - case gnu2x - case iso9899_1990 = "iso9899:1990" - case iso9899_199409 = "iso9899:199409" - case iso9899_1999 = "iso9899:1999" - case iso9899_2011 = "iso9899:2011" - case iso9899_2017 = "iso9899:2017" - case iso9899_2018 = "iso9899:2018" -} -``` - -# CXXLanguageStandard - -`enum CXXLanguageStandard` - -The supported C++ language standard to use for compiling C++ sources in the package. - -```swift -enum CXXLanguageStandard { - case cxx98 = "c++98" - case cxx03 = "c++03" - case cxx11 = "c++11" - case cxx14 = "c++14" - case cxx17 = "c++17" - case cxx1z = "c++1z" - case cxx20 = "c++20" - case cxx2b = "c++2b" - case gnucxx98 = "gnu++98" - case gnucxx03 = "gnu++03" - case gnucxx11 = "gnu++11" - case gnucxx14 = "gnu++14" - case gnucxx17 = "gnu++17" - case gnucxx1z = "gnu++1z" - case gnucxx20 = "gnu++20" - case gnucxx2b = "gnu++2b" -} -``` diff --git a/Documentation/PackageRegistry/PackageRegistryUsage.md b/Documentation/PackageRegistry/PackageRegistryUsage.md index 791ab103047..7e3b7087565 100644 --- a/Documentation/PackageRegistry/PackageRegistryUsage.md +++ b/Documentation/PackageRegistry/PackageRegistryUsage.md @@ -30,10 +30,10 @@ ## Getting Started SwiftPM supports downloading dependencies from any package registry that implements -[SE-0292](https://github.com/apple/swift-evolution/blob/main/proposals/0292-package-registry-service.md) +[SE-0292](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0292-package-registry-service.md) and the corresponding [service specification](Registry.md). -In a registry, packages are identified by [package identifier](https://github.com/apple/swift-evolution/blob/main/proposals/0292-package-registry-service.md#package-identity) +In a registry, packages are identified by [package identifier](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0292-package-registry-service.md#package-identity) in the form of `scope.package-name`. ### Configuring a registry @@ -92,7 +92,7 @@ resolve and download the appropriate release version. ### Registry authentication If a registry requires authentication, it can be set up by using the -[`swift package-registry login` subcommand](https://github.com/apple/swift-evolution/blob/main/proposals/0378-package-registry-auth.md#new-login-subcommand) +[`swift package-registry login` subcommand](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0378-package-registry-auth.md#new-login-subcommand) introduced by SE-0378: ```bash @@ -137,7 +137,7 @@ Here is an example of a source control dependency: ```swift dependencies: [ - .package(id: "https://github.com/mona/LinkedList", .upToNextMajor(from: "1.0.0")), + .package(url: "https://github.com/mona/LinkedList", .upToNextMajor(from: "1.0.0")), ], ``` @@ -158,7 +158,7 @@ any package identifier (e.g., `mona.LinkedList`). One can control if/how SwiftPM should use registry in conjunction with source control dependencies by setting one of these flags: - `--disable-scm-to-registry-transformation` (default): SwiftPM will not transform source control dependency to registry dependency. Source control dependency will be downloaded from its corresponding URL, while registry dependency will be resolved and downloaded using the configured registry (if any). -- `--use-registry-identity-for-scm`: SwiftPM will look up source control dependencies in the registry and use their registry identity whenever possible to help deduplicate packages across the two origins. In other words, suppose `mona.LinkedList` is the package identifer for `https://github.com/mona/LinkedList`, then SwiftPM will treat both references in the dependency graph as the same package. +- `--use-registry-identity-for-scm`: SwiftPM will look up source control dependencies in the registry and use their registry identity whenever possible to help deduplicate packages across the two origins. In other words, suppose `mona.LinkedList` is the package identifier for `https://github.com/mona/LinkedList`, then SwiftPM will treat both references in the dependency graph as the same package. - `--replace-scm-with-registry`: SwiftPM will look up source control dependencies in the registry and use the registry to retrieve them instead of source control when possible. In other words, SwiftPM will attempt to download a source control dependency from the registry first, and fall back to cloning the source repository iff the dependency is not found in the registry. ## Dependency Download From Registry @@ -183,11 +183,11 @@ or previous value, SwiftPM will fail the build. This can be tuned down from error to warning by setting the build option `--resolver-fingerprint-checking` to `warn` (default is `strict`). -Checkum TOFU is also done for manifests downloaded from registry. +Checksum TOFU is also done for manifests downloaded from registry. ### Validating signed packages -[SE-0391](https://github.com/apple/swift-evolution/blob/main/proposals/0391-package-registry-publish.md#package-signing) +[SE-0391](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0391-package-registry-publish.md#package-signing) adds package signing support to SwiftPM. SwiftPM determines if a downloaded archive is signed by checking for presence of the `X-Swift-Package-Signature-Format` and `X-Swift-Package-Signature` @@ -242,7 +242,7 @@ Data used by publisher TOFU is saved to `~/.swiftpm/security/signing-entities/`. ## Publishing to Registry -[`swift package-registry publish`](https://github.com/apple/swift-evolution/blob/main/proposals/0391-package-registry-publish.md#new-package-registry-publish-subcommand) +[`swift package-registry publish`](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0391-package-registry-publish.md#new-package-registry-publish-subcommand) is an all-in-one command for publishing a package release to registry: ```bash @@ -289,7 +289,7 @@ looks for a file named `package-metadata.json` in the package directory. Contents of the metadata file must conform to the -[JSON schema](https://github.com/apple/swift-evolution/blob/main/proposals/0391-package-registry-publish.md#package-release-metadata-standards) +[JSON schema](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0391-package-registry-publish.md#package-release-metadata-standards) defined in SE-0391. Also refer to registry documentation for any additional requirements. @@ -327,7 +327,7 @@ Refer to registry documentation for its certificate policy. | Signature Format | Specification | | ---------------- | ------------- | -| `cms-1.0.0` | [SE-391](https://github.com/apple/swift-evolution/blob/main/proposals/0391-package-registry-publish.md#package-signature-format-cms-100) | +| `cms-1.0.0` | [SE-391](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0391-package-registry-publish.md#package-signature-format-cms-100) | Since there is only one supported signature format, all signatures produced by SwiftPM are in `cms-1.0.0`. @@ -456,7 +456,7 @@ $ swift package-registry set --scope foo https://local.example.com $ swift package-registry set --scope foo --global https://global.example.com ``` -To remove a registry assignement, use the `swift package-registry unset` subcommand. +To remove a registry assignment, use the `swift package-registry unset` subcommand. ### Security configuration diff --git a/Documentation/PackageRegistry/Registry.md b/Documentation/PackageRegistry/Registry.md index c4667416aa4..b428dce6c9e 100644 --- a/Documentation/PackageRegistry/Registry.md +++ b/Documentation/PackageRegistry/Registry.md @@ -560,7 +560,7 @@ Content-Disposition: attachment; filename="Package.swift" Content-Length: 361 Content-Version: 1 Link: ; rel="alternate"; filename="Package@swift-4.swift"; swift-tools-version="4.0", - ; rel="alternate"; filename="Package@swift-4.2.swift"; swift-tools-version="4.0" + ; rel="alternate"; filename="Package@swift-4.2.swift"; swift-tools-version="4.2" // swift-tools-version:5.0 import PackageDescription @@ -1273,7 +1273,7 @@ JSON schema below. ```json { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/apple/swift-package-manager/blob/main/Documentation/PackageRegistry/Registry.md", + "$id": "https://github.com/swiftlang/swift-package-manager/blob/main/Documentation/PackageRegistry/Registry.md", "title": "Package Release Metadata", "description": "Metadata of a package release.", "type": "object", @@ -1427,6 +1427,6 @@ JSON schema below. [thundering herd effect]: https://en.wikipedia.org/wiki/Thundering_herd_problem "Thundering herd problem" [offline cache]: https://yarnpkg.com/features/offline-cache "Offline Cache | Yarn - Package Manager" [XCFramework]: https://developer.apple.com/videos/play/wwdc2019/416/ "WWDC 2019 Session 416: Binary Frameworks in Swift" -[SE-0272]: https://github.com/apple/swift-evolution/blob/master/proposals/0272-swiftpm-binary-dependencies.md "Package Manager Binary Dependencies" -[Swift tools version]: https://github.com/apple/swift-package-manager/blob/9b9bed7eaf0f38eeccd0d8ca06ae08f6689d1c3f/Documentation/Usage.md#swift-tools-version-specification "Swift Tools Version Specification" +[SE-0272]: https://github.com/swiftlang/swift-evolution/blob/master/proposals/0272-swiftpm-binary-dependencies.md "Package Manager Binary Dependencies" +[Swift tools version]: https://github.com/swiftlang/swift-package-manager/blob/9b9bed7eaf0f38eeccd0d8ca06ae08f6689d1c3f/Documentation/Usage.md#swift-tools-version-specification "Swift Tools Version Specification" [ISO 8601]: https://www.iso.org/iso-8601-date-and-time-format.html "ISO 8601 Date and Time Format" diff --git a/Documentation/PackageRegistry/registry.openapi.yaml b/Documentation/PackageRegistry/registry.openapi.yaml index 0db7124368e..99f9b5ddb16 100644 --- a/Documentation/PackageRegistry/registry.openapi.yaml +++ b/Documentation/PackageRegistry/registry.openapi.yaml @@ -1,448 +1,703 @@ -openapi: 3.0.0 +openapi: 3.0.3 info: - title: Swift Package Registry - version: "1" -externalDocs: - description: Swift Evolution Proposal SE-0292 - url: https://github.com/apple/swift-evolution/blob/main/proposals/0292-package-registry-service.md -servers: - - url: https://packages.swift.org + title: Swift Package Registry API + description: API for managing Swift package releases and interacting with the package registry. + version: 1.0.0 +tags: + - name: Releases + - name: Packages + - name: Authentication paths: - "/{scope}/{name}": + '/{scope}/{name}': parameters: - - $ref: "#/components/parameters/scope" - - $ref: "#/components/parameters/name" + - $ref: '#/components/parameters/Scope' + - $ref: '#/components/parameters/PackageName' get: tags: - - Package + - Releases summary: List package releases operationId: listPackageReleases - parameters: - - name: Content-Type - in: header - schema: - type: string - enum: - - application/vnd.swift.registry.v1+json responses: - "200": - description: "" + '200': + description: Retrieve a list of all available releases for a given package headers: + Link: + $ref: '#/components/headers/PackageLinks' Content-Version: - $ref: "#/components/headers/contentVersion" - Content-Length: - schema: - type: integer + $ref: '#/components/headers/ContentVersion' content: - application/json: + 'application/json': schema: - $ref: "#/components/schemas/releases" - examples: - default: - $ref: "#/components/examples/releases" - 4XX: - $ref: "#/components/responses/problemDetails" - "/{scope}/{name}/{version}": + $ref: '#/components/schemas/Releases' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '404': + $ref: '#/components/responses/NotFound' + '415': + $ref: '#/components/responses/UnsupportedMediaType' + '429': + $ref: '#/components/responses/TooManyRequests' + '/{scope}/{name}/{version}': parameters: - - $ref: "#/components/parameters/scope" - - $ref: "#/components/parameters/name" - - $ref: "#/components/parameters/version" + - $ref: '#/components/parameters/Scope' + - $ref: '#/components/parameters/PackageName' + - $ref: '#/components/parameters/Version' get: tags: - - Release - summary: Fetch release metadata + - Releases + summary: Fetch metadata for a package release operationId: fetchReleaseMetadata - parameters: - - name: Content-Type - in: header - schema: - type: string - enum: - - application/vnd.swift.registry.v1+json responses: - "200": - description: "" + '200': + description: Retrieve detailed metadata for a specific package release headers: + Link: + $ref: '#/components/headers/PackageLinks' Content-Version: - $ref: "#/components/headers/contentVersion" - Content-Length: - schema: - type: integer + $ref: '#/components/headers/ContentVersion' content: - application/json: + 'application/json': schema: - type: object - examples: - default: - $ref: "#/components/examples/metadata" - 4XX: - $ref: "#/components/responses/problemDetails" + $ref: '#/components/schemas/ReleaseMetadata' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '404': + $ref: '#/components/responses/NotFound' + '415': + $ref: '#/components/responses/UnsupportedMediaType' + '429': + $ref: '#/components/responses/TooManyRequests' put: tags: - - Release - summary: Publish package release - operationId: publishPackageRelease + - Releases + summary: Create a package release + operationId: createPackageRelease parameters: - - name: Content-Type - in: header + - in: header + name: X-Swift-Package-Signature-Format + description: The signature format if the source archive is signed. schema: type: string - enum: - - multipart/form-data + example: 'cms-1.0.0' + - in: header + name: Prefer + description: |- + Communicates the preference for sync vs async processing. + + Use "respond-async" to get async processing, for example "respond-async, wait=300" to wait up to 300 seconds. + + Omit this header to prefer sync processing. + schema: + type: string + requestBody: + description: A multipart payload. + required: true + content: + multipart/form-data: + schema: + type: object + properties: + source-archive: + type: string + format: binary + description: The binary source archive of the package to be published + example: 'Binary data for Linked List package release' + source-archive-signature: + type: string + format: binary + description: The signature for the source archive. + metadata: + $ref: '#/components/schemas/PackageMetadata' + metadata-signature: + type: string + format: base64 + description: The signature for the metadata. + required: + - source-archive + encoding: + source-archive: + contentType: application/zip + source-archive-signature: + contentType: application/octet-stream + metadata: + contentType: application/json + metadata-signature: + contentType: application/octet-stream responses: - "100": - description: "" - "201": - description: "" + '201': + description: The package release has been successfully published headers: Content-Version: - $ref: "#/components/headers/contentVersion" - Content-Length: + $ref: '#/components/headers/ContentVersion' + Location: + description: The URL to the created release. schema: - type: integer + type: string + format: uri + example: 'https://packages.example.com/github.com/mona/LinkedList/1.1.1' content: - application/json: + 'application/json': schema: - $ref: "#/components/schemas/releases" - examples: - default: - $ref: "#/components/examples/releases" - "202": - description: "" + $ref: '#/components/schemas/PublishResponse' + '202': + description: The request to publish the package has been accepted and is being processed + headers: + 'Retry-After': + $ref: '#/components/headers/RetryAfter' + Location: + description: |- + The URL to poll for status of the publication process. + + Poll that URL using a `GET` request, and if the response is 202, extract the Retry-After header to get a new estimate + of when the processing will be finished. + + When the response is 301, the processing is finished and the Location header points to the new release. + + If the response is 4xx, the processing failed and you can stop polling. The response contains the failure details. + + Send `DELETE` to the URL to cancel the processing. + schema: + type: string + format: uri + description: URL to poll for status of the publication process + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '404': + $ref: '#/components/responses/NotFound' + '405': + description: Method not allowed - publishing is not supported on this server. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/ProblemDetails' + '409': + description: Conflict - an existing release already exists, or the release is still processing. headers: - Content-Version: - $ref: "#/components/headers/contentVersion" Location: + description: The location of the release that's still processing, see the 202 response for details. schema: type: string - Retry-After: + format: uri + content: + application/problem+json: + schema: + $ref: '#/components/schemas/ProblemDetails' + '413': + description: Content too large. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/ProblemDetails' + '415': + $ref: '#/components/responses/UnsupportedMediaType' + '422': + description: Unprocessable entity - refused to publish the release. + content: + application/problem+json: schema: - type: integer - 4XX: - $ref: "#/components/responses/problemDetails" - "/{scope}/{name}/{version}/Package.swift": + $ref: '#/components/schemas/ProblemDetails' + '429': + $ref: '#/components/responses/TooManyRequests' + '/{scope}/{name}/{version}/Package.swift': parameters: - - $ref: "#/components/parameters/scope" - - $ref: "#/components/parameters/name" - - $ref: "#/components/parameters/version" + - $ref: '#/components/parameters/Scope' + - $ref: '#/components/parameters/PackageName' + - $ref: '#/components/parameters/Version' get: tags: - - Release + - Packages summary: Fetch manifest for a package release operationId: fetchManifestForPackageRelease parameters: - - name: Content-Type - in: header - schema: - type: string - enum: - - application/vnd.swift.registry.v1+swift - - $ref: "#/components/parameters/swift_version" + - $ref: '#/components/parameters/SwiftVersion' responses: - "200": - description: "" + '200': + description: Retrieve the manifest file for the specified package release headers: - Cache-Control: - schema: - type: string - Content-Disposition: - schema: - type: string - Content-Length: - schema: - type: integer - Content-Version: - $ref: "#/components/headers/optionalContentVersion" Link: + required: true schema: type: string + description: |- + One value for each version-specific package manifest file in the release's source archive, + whose filename matches the following regular expression pattern: + `\APackage@swift-(\d+)(?:\.(\d+))?(?:\.(\d+))?.swift\z` + + Each link value should have the alternate relation type, a filename attribute set to the version-specific package manifest filename + (for example, Package@swift-4.swift), and a swift-tools-version attribute set to the Swift tools version specified by the package + manifest file (for example, 4.0 for a manifest beginning with the comment // swift-tools-version:4.0). content: text/x-swift: schema: type: string - examples: - default: - $ref: "#/components/examples/manifest" - 4XX: - $ref: "#/components/responses/problemDetails" - "/{scope}/{name}/{version}.zip": + '303': + description: Redirect to the unqualified Package.swift resource. + headers: + Location: + required: true + schema: + type: string + format: uri + example: 'https://packages.example.com/mona/LinkedList/1.1.1/Package.swift' + Content-Version: + $ref: '#/components/headers/ContentVersion' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '404': + $ref: '#/components/responses/NotFound' + '415': + $ref: '#/components/responses/UnsupportedMediaType' + '429': + $ref: '#/components/responses/TooManyRequests' + '/{scope}/{name}/{version}.zip': parameters: - - $ref: "#/components/parameters/scope" - - $ref: "#/components/parameters/name" - - $ref: "#/components/parameters/version" + - $ref: '#/components/parameters/Scope' + - $ref: '#/components/parameters/PackageName' + - $ref: '#/components/parameters/Version' get: tags: - - Release - summary: Download source archive + - Packages + summary: Download source archive for a package release operationId: downloadSourceArchive - parameters: - - name: Content-Type - in: header - schema: - type: string - enum: - - application/vnd.swift.registry.v1+zip responses: - "200": - description: "" + '200': + description: Download the source archive for the specified package release headers: - Accept-Ranges: - schema: - type: string - Cache-Control: + Digest: + description: A digest containing a cryptographic digest of the source archive. schema: type: string + example: 'sha-256=oqxUzyX7wa0AKPA/CqS5aDO4O7BaFOUQiSuyfepNyBI=' Content-Disposition: + description: |- + A header set to attachment with a filename parameter equal to the name of the package followed by a hyphen (-), + the version number, and file extension (for example, "LinkedList-1.1.1.zip") schema: type: string - Content-Length: + example: 'attachment; filename="LinkedList-1.1.1.zip"' + X-Swift-Package-Signature-Format: + description: The format of the package signature, used for integrity verification schema: - type: integer - Content-Version: - $ref: "#/components/headers/optionalContentVersion" - Digest: - required: true + type: string + example: 'cms-1.0.0' + X-Swift-Package-Signature: + description: Signature of the downloaded package, used for integrity verification schema: type: string + format: base64 + example: 'l1TdTeIuGdNsO1FQ0ptD64F5nSSOsQ5WzhM6/7KsHRuLHfTsggnyIWr0DxMcBj5F40zfplwntXAgS0ynlqvlFw==' Link: + description: A header containing mirrors or multiple download locations. schema: type: string + description: A Link header with a `duplicate` relation, as described by RFC 6249. + example: '; rel=duplicate; geo=jp; pri=10; type="application/zip"' content: application/zip: schema: type: string format: binary - 3XX: - $ref: "#/components/responses/redirect" - 4XX: - $ref: "#/components/responses/problemDetails" - /identifiers: + '303': + description: See other - download from the URL in the Location header. + headers: + Location: + description: The location from which to download the resource. + required: true + schema: + type: string + example: 'https://example.cdn.com/LinkedList-1.1.1.zip?key=XXXXXXXXXXXXXXXXX' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '404': + $ref: '#/components/responses/NotFound' + '415': + $ref: '#/components/responses/UnsupportedMediaType' + '429': + $ref: '#/components/responses/TooManyRequests' + '/identifiers': parameters: - - $ref: "#/components/parameters/url" + - $ref: '#/components/parameters/Url' get: tags: - - Package + - Packages summary: Lookup package identifiers registered for a URL operationId: lookupPackageIdentifiersByURL - parameters: - - name: Content-Type - in: header - schema: - type: string - enum: - - application/vnd.swift.registry.v1+json responses: - "200": - description: "" + '200': + description: Retrieve a list of package identifiers registered for a specific URL headers: Content-Version: - $ref: "#/components/headers/contentVersion" + $ref: '#/components/headers/ContentVersion' content: - application/json: + 'application/json': schema: - $ref: "#/components/schemas/identifiers" - examples: - default: - $ref: "#/components/examples/identifiers" - 4XX: - $ref: "#/components/responses/problemDetails" + $ref: '#/components/schemas/Identifiers' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '415': + $ref: '#/components/responses/UnsupportedMediaType' + '429': + $ref: '#/components/responses/TooManyRequests' + '/login': + post: + tags: + - Authentication + summary: Log in to the package registry + operationId: loginToRegistry + description: |- + Log in using either basic or token authentication. Use the `Authorization` header to provide credentials. + + Also use this endpoint to verify authentication credentials before saving them to the local secrets store. + responses: + '200': + description: User successfully logged in to the package registry + '401': + $ref: '#/components/responses/Unauthorized' + '429': + $ref: '#/components/responses/TooManyRequests' + '501': + description: Registry does not support authentication. components: - schemas: - releases: - type: object - example: - releases: - 1.1.0: https://swift.pkg.github.com/mona/LinkedList/1.1.0 - properties: - releases: - type: object - required: - - releases - identifiers: - type: object - example: - identifiers: - - "mona.LinkedList" - properties: - identifiers: - type: array - items: - type: string - required: - - identifiers - problem: - type: object - externalDocs: - url: https://tools.ietf.org/html/rfc7807 - example: - instance: /account/12345/msgs/abc - balance: 30 - type: https://example.com/probs/out-of-credit - title: You do not have enough credit. - accounts: - - /account/12345 - - /account/67890 - detail: Your current balance is 30, but that costs 50. - properties: - type: - type: string - format: uriref - title: - type: string - status: - type: number - instance: - type: string - detail: - type: string - required: - - detail - - instance - - status - - title - - type - responses: - problemDetails: - description: A client error. - headers: - Content-Version: - $ref: "#/components/headers/contentVersion" - Content-Language: - schema: - type: string - Content-Length: - schema: - type: integer - content: - application/problem+json: - schema: - $ref: "#/components/schemas/problem" - redirect: - description: A server redirect. - headers: - Content-Version: - $ref: "#/components/headers/contentVersion" - Location: - schema: - type: string - Digest: - schema: - type: string - Content-Length: - schema: - type: integer parameters: - scope: + SwiftVersion: + name: swift-version + in: query + required: false + description: The Swift version for which to fetch the manifest + schema: + type: string + example: '4.2' + Scope: name: scope in: path required: true + description: |- + The scope of the package (for example, the organization or user). + Package scopes are case-insensitive. schema: type: string - example: "mona" - pattern: \A[a-zA-Z\d](?:[a-zA-Z\d]|-(?=[a-zA-Z\d])){0,38}\z - name: + pattern: '[a-zA-Z0-9](?:[a-zA-Z0-9]|-(?=[a-zA-Z0-9])){0,38}' + example: 'mona' + PackageName: name: name in: path required: true + description: |- + The name of the package. + Package names are case-insensitive. schema: type: string - example: LinkedList - pattern: \A[a-zA-Z0-9](?:[a-zA-Z0-9]|[-_](?=[a-zA-Z0-9])){0,99}\z - version: + pattern: '[a-zA-Z0-9](?:[a-zA-Z0-9]|[-_](?=[a-zA-Z0-9])){0,99}' + example: 'LinkedList' + Version: name: version in: path required: true + description: The semantic version of the package release. schema: type: string - externalDocs: - description: Semantic Version number - url: https://semver.org - example: 1.0.0-beta.1 - pattern: ^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$ - swift_version: - name: swift-version - in: query - schema: - type: string - example: 1.2.3 - pattern: \d+(?:\.(\d+)){0,2} - url: + example: '1.2.3' + Url: name: url in: query required: true + description: The URL for which to look up registered package identifiers schema: type: string - format: url - example: https://github.com/mona/LinkedList - examples: - releases: - value: - releases: - 1.1.1: - url: https://swift.pkg.github.com/mona/LinkedList/1.1.1 - 1.1.0: - problem: - status: 410 - title: Gone - detail: this release was removed from the registry - url: https://swift.pkg.github.com/mona/LinkedList/1.1.0 - 1.0.0: - url: https://swift.pkg.github.com/mona/LinkedList/1.0.0 - manifest: - value: >- - // swift-tools-version:5.0 + example: 'https://example.com/mona/LinkedList' + responses: + Unauthorized: + description: Authentication failed due to invalid credentials provided + content: + application/problem+json: + schema: + $ref: '#/components/schemas/ProblemDetails' + BadRequest: + description: Bad Request - The request was invalid or cannot be otherwise served + content: + application/problem+json: + schema: + $ref: '#/components/schemas/ProblemDetails' + NotFound: + description: Not Found - The specified resource could not be found + content: + application/problem+json: + schema: + $ref: '#/components/schemas/ProblemDetails' + UnsupportedMediaType: + description: Unsupported Media Type - Accept header specifies a valid but unsupported API version. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/ProblemDetails' + TooManyRequests: + description: Too Many Requests - Rate limit exceeded + headers: + 'Retry-After': + $ref: '#/components/headers/RetryAfter' + content: + application/problem+json: + schema: + $ref: '#/components/schemas/ProblemDetails' + headers: + PackageLinks: + schema: + type: string + description: |- + A set of related links. - import PackageDescription + An example: + ; rel="canonical", + ; rel="alternate", + ; rel="latest-version", + ; rel="payment", + ; rel="first", + ; rel="previous", + ; rel="next", + ; rel="last" + Another example: + ; rel="latest-version", + ; rel="predecessor-version" - let package = Package( - name: "LinkedList", - products: [ - .library(name: "LinkedList", targets: ["LinkedList"]) - ], - targets: [ - .target(name: "LinkedList"), - .testTarget(name: "LinkedListTests", dependencies: ["LinkedList"]), - ], - swiftLanguageVersions: [.v4, .v5] - ) - metadata: - value: - keywords: - - data-structure - - collection - version: 1.1.1 - "@type": SoftwareSourceCode - author: - "@type": Person - "@id": https://github.com/mona - middleName: Lisa - givenName: Mona - familyName: Octocat - license: https://www.apache.org/licenses/LICENSE-2.0 - programmingLanguage: - url: https://swift.org - name: Swift - "@type": ComputerLanguage - codeRepository: https://github.com/mona/LinkedList - "@context": - - http://schema.org/ - description: One thing links to another. - name: LinkedList - identifiers: - value: - identifiers: - - "mona.LinkedList" - headers: - contentVersion: - required: true + Also see: + - https://www.rfc-editor.org/rfc/rfc6596.html + - https://html.spec.whatwg.org/multipage/links.html#link-type-alternate + - https://github.com/swiftlang/swift-package-manager/blob/main/Documentation/PackageRegistry/Registry.md#4-endpoints + RetryAfter: schema: type: string - enum: - - "1" - optionalContentVersion: - required: false + description: |- + Retry-After header, see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After. + + Examples: + Retry-After: Wed, 21 Oct 2015 07:28:00 GMT + Retry-After: 120 + example: '120' + ContentVersion: + description: The content version. + required: true schema: - type: string + type: integer enum: - - "1" + - 1 + schemas: + PackageOrganization: + type: object + properties: + description: + description: A description of the organization. + type: string + email: + description: Email address of the organization. + format: email + type: string + name: + description: Name of the organization. + type: string + url: + description: URL of the organization. + format: uri + type: string + required: + - name + PackageAuthor: + type: object + properties: + description: + description: A description of the author. + type: string + email: + description: Email address of the author. + format: email + type: string + name: + description: Name of the author. + type: string + organization: + $ref: '#/components/schemas/PackageOrganization' + url: + description: URL of the author. + format: uri + type: string + required: + - name + PackageMetadata: + type: object + description: Metadata of a package release. + properties: + author: + $ref: '#/components/schemas/PackageAuthor' + description: + type: string + description: A description of the package release. + licenseURL: + type: string + format: uri + description: URL of the package release's license document. + originalPublicationTime: + type: string + format: date-time + description: Original publication time of the package release in ISO 8601 format. + readmeURL: + type: string + format: uri + description: URL of the README specifically for the package release or broadly + for the package. + repositoryURLs: + type: array + items: + description: Code repository URL. + type: string + format: uri + description: Code repository URL(s) of the package release. + PublishResponse: + type: object + properties: + message: + type: string + example: 'Package release successfully published' + url: + type: string + format: uri + description: The URL of the newly published package release + example: 'https://packages.example.com/mona/LinkedList/1.1.0' + ProblemDetails: + type: object + description: A problem detail object conforming to RFC7807 + properties: + type: + type: string + format: uri + description: |- + A URI reference that identifies the problem type. + This specification encourages that, when dereferenced, + it provides human-readable documentation for the problem type. + example: 'https://example.com/problem/invalid-request-parameters' + title: + type: string + description: |- + A short, human-readable summary of the problem type. + It should not change from occurrence to occurrence of the + problem, except for purposes of localization. + example: 'Invalid request parameters' + status: + type: integer + description: |- + The HTTP status code generated by the origin server for this occurrence of the problem. + example: 400 + detail: + type: string + description: A human-readable explanation specific to this occurrence of the problem. + example: 'The "name" field is required.' + instance: + type: string + format: uri + description: A URI reference that identifies the specific occurrence of the problem. + example: '/requests/12345' + additionalProperties: true + ListedRelease: + type: object + properties: + url: + type: string + format: uri + example: 'https://packages.example.com/mona/LinkedList/1.1.0' + problem: + $ref: '#/components/schemas/ProblemDetails' + Releases: + type: object + properties: + releases: + type: object + additionalProperties: + $ref: '#/components/schemas/ListedRelease' + required: + - releases + ReleaseSignature: + type: object + properties: + signatureBase64Encoded: + type: string + format: base64 + description: The resource's signature, base64 encoded. + example: 'dGVzdFNpZ25hdHVyZQ==' + signatureFormat: + type: string + description: The signature format. + example: 'cms-1.0.0' + required: + - signatureBase64Encoded + - signatureFormat + ReleaseResource: + type: object + properties: + name: + type: string + description: The name of the resource. + example: 'source-archive' + type: + type: string + description: The content type of the resource. + example: 'application/zip' + checksum: + type: string + description: A hexadecimal representation of the SHA256 digest for the resource. + example: 'efaa6545cd99dd1e124b5269ea0586994b6d97a0443021d2966e1df6dec9c4a1' + signing: + $ref: '#/components/schemas/ReleaseSignature' + required: + - name + - type + - checksum + ReleaseMetadata: + type: object + properties: + id: + $ref: '#/components/schemas/Identifier' + version: + type: string + description: The package release version number. + example: '1.2.3' + resources: + type: array + items: + $ref: '#/components/schemas/ReleaseResource' + description: The resources available for the release. + metadata: + $ref: '#/components/schemas/PackageMetadata' + publishedAt: + type: string + format: date-time + description: |- + The ISO 8601-formatted datetime string of when the package release was published, as recorded by the registry. + See related `originalPublicationTime` in metadata. + required: + - id + - version + - resources + - metadata + Identifier: + type: string + example: 'mona.LinkedList' + Identifiers: + type: object + properties: + identifiers: + type: array + items: + $ref: '#/components/schemas/Identifier' + required: + - identifiers diff --git a/Documentation/PackageSecurity.md b/Documentation/PackageSecurity.md deleted file mode 100644 index a89c7adee0a..00000000000 --- a/Documentation/PackageSecurity.md +++ /dev/null @@ -1,45 +0,0 @@ -# Package Security - -This document provides an overview of security features that SwiftPM implements. - -## Trust on First Use - -SwiftPM records **fingerprints** of downloaded package versions so that -it can perform [trust-on-first-use](https://en.wikipedia.org/wiki/Trust_on_first_use) -(TOFU). That is, when a package version is downloaded for the first time, -SwiftPM trusts that it has downloaded the correct contents and requires -subsequent downloads of the same package version to have the same -fingerprint. If the fingerprint changes, it might be an indicator that the -package has been compromised and SwiftPM will either warn or return an error. - -Depending on where a package version is downloaded from, different value is -used as its fingerprint: - -| Package Version Origin | Fingerprint | -| ---------------------- | ----------- | -| Git repository | Git hash of the revision | -| Package registry | Checksum of the source archive | - -All version fingerprints of a package are kept in a single file -under the `~/.swiftpm/security/fingerprints` directory. - - For a Git repository package, the fingerprint filename takes the form of `{PACKAGE_NAME}-{REPOSITORY_URL_HASH}.json` (e.g., `LinkedList-5ddbcf15.json`). - - For a registry package, the fingerprint filename takes the form of `{PACKAGE_ID}.json` (e.g., `mona.LinkedList.json`). - -### Using fingerprints for TOFU - -When a package version is downloaded from Git repository or registry for -the first time, SwiftPM simply saves the fingerprint to the designated -file in the `~/.swiftpm/security/fingerprints` directory. - -Otherwise, SwiftPM compares fingerprint of the downloaded package version -with that saved from previous download. The two fingerprint values must match or -else SwiftPM will throw an error. This can be tuned down to warning by setting -the build option `--resolver-fingerprint-checking` to `warn` (default is `strict`). - -Note that in case of registry packages, a package version's fingerprint -must be consistent across registries or else there will be a TOFU failure. -As an example, suppose a package version was originially downloaded from -registry A and the source archive checksum was saved as the fingerprint. Later -the package version is downloaded again but from registry B and has a different -fingerprint. This would trigger a TOFU failure since the fingerprint should -be consistent across the registries. diff --git a/Documentation/Plugins.md b/Documentation/Plugins.md deleted file mode 100644 index c9722185053..00000000000 --- a/Documentation/Plugins.md +++ /dev/null @@ -1,383 +0,0 @@ -# Getting Started with Plugins - -This guide provides a brief overview of Swift Package Manager plugins, describes how a package can make use of plugins, and shows how to get started writing your own plugins. - -## Overview - -Some of Swift Package Manager's functionality can be extended through _plugins_. Package plugins are written in Swift using the `PackagePlugin` API provided by the Swift Package Manager. This is similar to how the Swift Package manifest itself is implemented as a Swift script that runs as needed in order to produce the information SwiftPM needs. - -A plugin is represented in the SwiftPM package manifest as a target of the `pluginTarget` type — and if it should be available to other packages, there also needs to be a corresponding `pluginProduct` target. Source code for a plugin is normally located in a directory under the `Plugins` directory in the package, but this can be customized. - -SwiftPM currently defines two extension points for plugins: - -- custom build tool tasks that provide commands to run before or during the build -- custom commands that are run using the `swift package` command line interface - -A plugin declares which extension point it implements by defining the plugin's _capability_. This determines the entry point through which SwiftPM will call it, and determines which actions the plugin can perform. - -Plugins have access to a representation of the package model, and plugins that define custom commands can also invoke services provided by SwiftPM to build and test products and targets defined in the package to which the plugin is applied. - -Every plugin runs as a separate process, and (on platforms that support sandboxing) it is wrapped in a sandbox that prevents network access as well as attempts to write to arbitrary locations in the file system. Custom command plugins that need to modify the package source code can specify this requirement, and if the user approves, will have write access to the package directory. Build tool plugins cannot modify the package source code. All plugins can write to a temporary directory. - -## Using a Package Plugin - -A package plugin is available to the package that defines it, and if there is a corresponding plugin product, it is also available to any other package that has a direct dependency on the package that defines it. - -To get access to a plugin defined in another package, add a package dependency on the package that defines the plugin. This will let the package access any build tool plugins and command plugins from the dependency. - -### Making use of a build tool plugin - -To make use of a build tool plugin, list its name in each target to which it should apply: - -```swift -// swift-tools-version: 5.6 -import PackageDescription - -let package = Package( - name: "my-plugin-example", - dependencies: [ - .package(url: "https://github.com/example/my-plugin-package.git", from: "1.0"), - ], - targets: [ - .executableTarget( - name: "MyExample", - plugins: [ - .plugin(name: "MyBuildToolPlugin", package: "my-plugin-package"), - ] - ) - ] -) -``` - -This will cause SwiftPM to call the plugin, passing it a simplified version of the package model for the target to which it is being applied. Any build commands returned by the plugin will be incorporated into the build graph and will run at the appropriate time during the build. - -### Making use of a command plugin - -Unlike build tool plugins, which are invoked as needed when SwiftPM constructs the build task graph, command plugins are only invoked directly by the user. This is done through the `swift` `package` command line interface: - -```shell -❯ swift package my-plugin --my-flag my-parameter -``` - -Any command line arguments that appear after the invocation verb defined by the plugin are passed unmodified to the plugin — in this case, `--my-flag` and `my-parameter`. This is commonly used in order to narrow down the application of a command to one or more targets, through the convention of one or more occurrences of a `--target` option with the name of the target(s). - -To list the plugins that are available within the context of a package, use the `--list` option of the `plugin` subcommand: - -```shell -❯ swift package plugin --list -``` - -Command plugins that need to write to the file system will cause SwiftPM to ask the user for approval if `swift package` is invoked from a console, or deny the request if it is not. Passing the `--allow-writing-to-package-directory` flag to the `swift package` invocation will allow the request without questions — this is particularly useful in a Continuous Integration environment. Similarly, the `--allow-network-connections` flag can be used to allow network connections without showing a prompt. - -## Writing a Plugin - -The first step when writing a package plugin is to decide what kind of plugin you need. If your goal is to generate source files that should be part of a build, or to perform other actions at the start of every build, implement a build tool plugin. If your goal is to provide actions that users can perform at any time and that are not associated with a build, implement a command plugin. - -### Build tool plugins - -Build tool plugins are invoked before a package is built in order to construct command invocations to run as part of the build. There are two kinds of commands that a build tool plugin can return: - -- prebuild commands — are run before the build starts and can generate an arbitrary number of output files with names that can't be predicted before running the command -- build commands — are incorporated into the build system's dependency graph and will run at the appropriate time during the build based on the existence and timestamps of their predefined inputs and outputs - -Build commands are preferred over prebuild commands when the paths of all of the inputs and outputs are known before the command runs, since they allow the build system to more efficiently decide when they should be run. This is actually quite common. Examples include source translation tools that generate one output file (with a predictable name) for each input file, or other cases where the plugin can control the names of the outputs without having to first run the tool. In this case the build system can run the command only when some of the outputs are missing or when the inputs have changed since the last time the command ran. There doesn't have to be a one-to-one correspondence between inputs and outputs; a plugin is free to choose how many (if any) output files to create by examining the input target using any logic it wants to. - -Prebuild commands should be used only when the names of the outputs are not known until the tool is run — this is the case if the _contents_ of the input files (as opposed to just their names) determines the number and names of the output files. Prebuild commands have to run before every build, and should therefore do their own caching to do as little work as possible to avoid slowing down incremental builds. - -In either case, it is important to note that it is not the plugin itself that does all the work of the build command — rather, the plugin constructs the commands that will later need to run, and it is those commands that perform the actual work. The plugin itself is usually quite small and is mostly concerned with forming the command line for the build command that does the actual work. - -#### Declaring a build tool plugin in the package manifest - -Like all kinds of package plugins, build tool plugins are declared in the package manifest. This is done using a `pluginTarget` entry in the `targets` section of the package. If the plugin should be visible to other packages, there needs to be a corresponding `plugin` entry in the `products` section as well: - -```swift -// swift-tools-version: 5.6 -import PackageDescription - -let package = Package( - name: "MyPluginPackage", - products: [ - .plugin( - name: "MyBuildToolPlugin", - targets: [ - "MyBuildToolPlugin" - ] - ) - ], - dependencies: [ - .package( - url: "https://github.com/example/sometool", - from: "0.1.0" - ) - ], - targets: [ - .plugin( - name: "MyBuildToolPlugin", - capability: .buildTool(), - dependencies: [ - .product(name: "SomeTool", package: "sometool"), - ] - ) - ] -) -``` - -The `plugin` target declares the name and capability of the plugin, along with its dependencies. The capability of `.buildTool()` is what declares it as a build tool plugin as opposed to any other kind of plugin — this also determines what entry point the plugin is expected to implement (as described below). - -The Swift script files that implement the logic of the plugin are expected to be in a directory named the same as the plugin, located under the `Plugins` subdirectory of the package. This can be overridden with a `path` parameter in the `pluginTarget`. - -The `plugin` product is what makes the plugin visible to other packages that have dependencies on the package that defines the plugin. The name of the plugin doesn't have to match the name of the product, but they are often the same in order to avoid confusion. The plugin product should list only the name of the plugin target it vends. If a built tool plugin is used only within the package that declares it, there is no need to declare a `plugin` product. - -#### Build tool target dependencies - -The dependencies specify the command line tools that will be available for use in commands constructed by the plugin. Each dependency can be either an `executableTarget` or a `binaryTarget` target in the same package, or can be an `executable` product in another package (there are no binary products in SwiftPM). In the example above, the plugin depends on the hypothetical _SomeTool_ product in the _sometool_ package on which the package that defines the plugin has a dependency. Note that this does not necessarily mean that _SomeTool_ will have been built when the plugin is invoked — it only means that the plugin will be able to look up the path at which the tool will exist at the time any commands constructed by the plugin are run. - -Executable dependencies are built for the host platform as part of the build, while binary dependencies are references to `artifactbundle` archives that contains prebuilt binaries (see [SE-305](https://github.com/apple/swift-evolution/blob/main/proposals/0305-swiftpm-binary-target-improvements.md)). Binary targets are often used when the tool is built using a different build system than SwiftPM, or when building it on demand is prohibitively expensive or requires a special build environment. - -#### Implementing the build tool plugin script - -By default, Swift Package Manager looks for the implementation of a declared plugin in a subdirectory of the `Plugins` directory named with the same name as the plugin target. This can be overridden using the `path` parameter in the target declaration. - -A plugin consists of one or more Swift source files, and the main entry point of the build tool plugin script is expected to conform to the `BuildToolPlugin` protocol. - -Similar to how a package manifest imports the *PackageDescription* module provided by SwiftPM, a package plugin imports the *PackagePlugin* module which contains the API through which the plugin receives information from SwiftPM and communicates results back to it. - -```swift -import PackagePlugin - -@main -struct MyPlugin: BuildToolPlugin { - - func createBuildCommands(context: PluginContext, target: Target) throws -> [Command] { - guard let target = target.sourceModule else { return [] } - let inputFiles = target.sourceFiles.filter({ $0.path.extension == "dat" }) - return try inputFiles.map { - let inputFile = $0 - let inputPath = inputFile.path - let outputName = inputPath.stem + ".swift" - let outputPath = context.pluginWorkDirectory.appending(outputName) - return .buildCommand( - displayName: "Generating \(outputName) from \(inputPath.lastComponent)", - executable: try context.tool(named: "SomeTool").path, - arguments: [ "--verbose", "\(inputPath)", "\(outputPath)" ], - inputFiles: [ inputPath, ], - outputFiles: [ outputPath ] - ) - } - } -} -``` - -The plugin script can import *Foundation* and other standard libraries, but in the current version of SwiftPM, it cannot import other libraries. - -In this example, the returned command is of the type `buildCommand`, so it will be incorporated into the build system's command graph and will run if any of the output files are missing or if the contents of any of the input files have changed since the last time the command ran. - -Note that build tool plugins are always applied to a target, which is passed in the parameter to the entry point. Only source module targets have source files, so a plugin that iterates over source files will commonly test that the target it was given conforms to `SourceModuleTarget`. - -A build tool plugin can also return commands of the type `prebuildCommand`, which run before the build starts and can populate a directory with output files whose names are not known until the command runs: - -```swift -import PackagePlugin -import Foundation - -@main -struct MyBuildToolPlugin: BuildToolPlugin { - - func createBuildCommands(context: PluginContext, target: Target) throws -> [Command] { - // This example configures `sometool` to write to a "GeneratedFiles" directory in - // the plugin work directory (which is unique for each plugin and target). - let outputDir = context.pluginWorkDirectory.appending("GeneratedFiles") - try FileManager.default.createDirectory(atPath: outputDir.string, - withIntermediateDirectories: true) - - // Return a command to run `sometool` as a prebuild command. It will be run before - // every build and generates source files into an output directory provided by the - // build context. - return [.prebuildCommand( - displayName: "Running SomeTool", - executable: try context.tool(named: "SomeTool").path, - arguments: [ "--verbose", "--outdir", outputDir ], - outputFilesDirectory: outputDir) - ] - } -} -``` - -In the case of prebuild commands, any dependencies must be binary targets, since these commands run before the build starts. - -Note that a build tool plugin can return a combination of build tool commands and prebuild commands. After the plugin runs, any build commands are incorporated into the build graph, which may result in changes that require commands to run during the subsequent build. - -Any prebuild commands are run after the plugin runs but before the build starts, and any files that are in the prebuild command's declared `outputFilesDirectory` will be evaluated as if they had been source files in the target. The prebuild command should add or remove files in this directory to reflect the results of having run the command. - -The current version of the Swift Package Manager supports generated Swift source files and resources as outputs, but it does not yet support non-Swift source files. Any generated resources are processed as if they had been declared in the manifest with the `.process()` rule. The intent is to eventually support any type of file that could have been included as a source file in the target, and to let the plugin provide greater controls over the downstream processing of generated files. - -### Command plugins - -Command plugins are invoked at will by the user, by invoking `swift` `package` `` ``. They are unrelated to the build graph, and often perform their work by invoking to command line tools as subprocesses. - -Command plugins are declared in a similar way to build tool plugins, except that they declare a `.command()` capability and implement a different entry point in the plugin script. - -A command plugin specifies the semantic intent of the command — this might be one of the predefined intents such “documentation generation” or “source code formatting”, or it might be a custom intent with a specialized verb that can be passed to the `swift` `package` command. A command plugin can also specify any special permissions it needs, such as the permission to modify the files under the package directory. - -The command's intent declaration provides a way of grouping command plugins by their functional categories, so that SwiftPM — or an IDE that supports SwiftPM packages — can show the commands that are available for a particular purpose. For example, this approach supports having different command plugins for generating documentation for a package, while still allowing those different commands to be grouped and discovered by intent. - -#### Declaring a command plugin in the package manifest - -The manifest of a package that declares a command plugin might look like: - -```swift -// swift-tools-version: 5.6 -import PackageDescription - -let package = Package( - name: "MyPluginPackage", - products: [ - .plugin( - name: "MyCommandPlugin", - targets: [ - "MyCommandPlugin" - ] - ) - ], - dependencies: [ - .package( - url: "https://github.com/example/sometool", - from: "0.1.0" - ) - ], - targets: [ - .plugin( - name: "MyCommandPlugin", - capability: .command( - intent: .sourceCodeFormatting(), - permissions: [ - .writeToPackageDirectory(reason: "This command reformats source files") - ] - ), - dependencies: [ - .product(name: "SomeTool", package: "sometool"), - ] - ) - ] -) -``` - -Here the plugin declares that its purpose is source code formatting, and specifically declares that it will need permission to modify files in the package directory. Plugins are run in a sandbox that prevents network access and most file system access, but declarations about the need to write to the package add those permissions to the sandbox (after asking the user to approve). - -#### Implementing the command plugin script - -As with build tool plugins, the scripts that implement command plugins should be located under the `Plugins` subdirectory in the package. - -For a command plugin the entry point of the plugin script is expected to conform to the `CommandPlugin` protocol: - -```swift -import PackagePlugin -import Foundation - -@main -struct MyCommandPlugin: CommandPlugin { - - func performCommand( - context: PluginContext, - arguments: [String] - ) throws { - // We'll be invoking `sometool` to format code, so start by locating it. - let sometool = try context.tool(named: "sometool") - - // By convention, use a configuration file in the root directory of the - // package. This allows package owners to commit their format settings - // to their repository. - let configFile = context.package.directory.appending(".sometoolconfig") - - // Extract the target arguments (if there are none, we assume all). - var argExtractor = ArgumentExtractor(arguments) - let targetNames = argExtractor.extractOption(named: "target") - let targets = targetNames.isEmpty - ? context.package.targets - : try context.package.targets(named: targetNames) - - // Iterate over the targets we've been asked to format. - for target in targets { - // Skip any type of target that doesn't have source files. - // Note: We could choose to instead emit a warning or error here. - guard let target = target.sourceModule else { continue } - - // Invoke `sometool` on the target directory, passing a configuration - // file from the package directory. - let sometoolExec = URL(fileURLWithPath: sometool.path.string) - let sometoolArgs = [ - "--config", "\(configFile)", - "--cache", "\(context.pluginWorkDirectory.appending("cache-dir"))", - "\(target.directory)" - ] - let process = try Process.run(sometoolExec, arguments: sometoolArgs) - process.waitUntilExit() - - // Check whether the subprocess invocation was successful. - if process.terminationReason == .exit && process.terminationStatus == 0 { - print("Formatted the source code in \(target.directory).") - } - else { - let problem = "\(process.terminationReason):\(process.terminationStatus)" - Diagnostics.error("Formatting invocation failed: \(problem)") - } - } - } -} -``` - -Unlike build tool plugins, which are always applied to a single package target, a command plugin does not necessarily operate on just a single target. The `context` parameter provides access to the inputs, including to a distilled version of the package graph rooted at the package to which the command plugin is applied. - -Command plugins can also accept arguments, which can control options for the plugin's actions or can further narrow down what the plugin operates on. This example supports the convention of passing `--target` to limit the scope of the plugin to a set of targets in the package. - -In the current version of Swift Package Manager, plugins can only use standard system libraries (and not those from other packages, such as SwiftArgumentParser). Consequently, this plugin uses the built-in `ArgumentExtractor` helper in the *PackagePlugin* module to do simple argument extraction. - -### Diagnostics - -Plugin entry points are marked `throws`, and any errors thrown from the entry point cause the plugin invocation to be marked as having failed. The thrown error is presented to the user, and should include a clear description of what went wrong. - -Additionally, plugins can use the `Diagnostics` API in PackagePlugin to emit warnings and errors that optionally include references to file paths and line numbers in those files. - -### Debugging and Testing - -SwiftPM doesn't currently have any specific support for debugging and testing plugins. Many plugins act only as adapters that construct command lines for invoking the tools that do the real work — in the cases in which there is non-trivial code in a plugin, the best current approach is to factor out that code into separate source files that can be included in unit tests in the plugin package via symbolic links with relative paths. - -### Xcode Extensions to the PackagePlugin API - -When invoked in Apple’s Xcode IDE, plugins have access to a library module provided by Xcode called *XcodeProjectPlugin* — this module extends the *PackagePlugin* APIs to let plugins work on Xcode targets in addition to packages. - -In order to write a plugin that works with packages in every environment and that conditionally works with Xcode projects when run in Xcode, the plugin should conditionally import the *XcodeProjectPlugin* module when it is available. For example: - -```swift -import PackagePlugin - -@main -struct MyCommandPlugin: CommandPlugin { - /// This entry point is called when operating on a Swift package. - func performCommand(context: PluginContext, arguments: [String]) throws { - debugPrint(context) - } -} - -#if canImport(XcodeProjectPlugin) -import XcodeProjectPlugin - -extension MyCommandPlugin: XcodeCommandPlugin { - /// This entry point is called when operating on an Xcode project. - func performCommand(context: XcodePluginContext, arguments: [String]) throws { - debugPrint(context) - } -} -#endif -``` - -The `XcodePluginContext` input structure is similar to the regular `PluginContext` structure, except that it provides access to an Xcode project that uses Xcode naming and semantics for the project model (which is somewhat different from that of SwiftPM). Some of the underlying types, such as `FileList`, `Path`, etc are the same for `PackagePlugin` and `XcodeProjectPlugin` types. - -If any targets are chosen in the Xcode user interface, Xcode passes their names as `--target` arguments to the plugin. - -It is expected that other IDEs or custom environments that use SwiftPM could similarly provide modules that define new entry points and extend the functionality of the core `PackagePlugin` APIs. - -### References - -- "Meet Swift Package plugins" [WWDC22 session](https://developer.apple.com/videos/play/wwdc2022-110359) -- "Create Swift Package plugins" [WWDC22 session](https://developer.apple.com/videos/play/wwdc2022-110401) diff --git a/Documentation/README.md b/Documentation/README.md index b9533baacc6..4cf63c72e42 100644 --- a/Documentation/README.md +++ b/Documentation/README.md @@ -1,106 +1,9 @@ # Swift Package Manager -The Swift Package Manager is a tool for managing distribution of source code, aimed at making it easy to share your code and reuse others’ code. The tool directly addresses the challenges of compiling and linking Swift packages, managing dependencies, versioning, and supporting flexible distribution and collaboration models. +The documentation previously included here has primarily been migrated into DocC format, and is available for viewing online at Swift.org [Package Manager Docs](https://docs.swift.org/swiftpm/documentation/packagemanagerdocs). +The sources for the documentation content are in the [Sources/PackageManagerDocs](Sources/PackageManagerDocs) directory. -We’ve designed the system to make it really easy to share packages on services like GitHub, but packages are also great for private personal development, sharing code within a team, or at any other granularity. +For documentation on developing the Swift Package Manager itself, see the [contribution guide](CONTRIBUTING.md). -*** - -## Table of Contents - -* [**Overview**](README.md) - * [About Packages](#about-packages) - * [About Modules](#about-modules) - * [Building Swift Modules](#building-swift-modules) - * [About Products](#about-products) - * [About Dependencies](#about-dependencies) - * [Dependency Hell](#dependency-hell) -* [Using packages](Usage.md) -* [Package manifest specification](PackageDescription.md) -* [Getting Started with Plugins](Plugins.md) -* [Package discovery with Package Collections](PackageCollections.md) -* [Package Registry service specification](PackageRegistry/Registry.md) -* [Using SwiftPM as a library](libSwiftPM.md) -* [Using Module Aliasing](ModuleAliasing.md) - -*** - -## About Packages - -A package consists of Swift source files, including the `Package.swift` manifest file. The manifest file, or package manifest, defines the package's name and its contents using the PackageDescription module. A package has one or more targets. Each target specifies a product and may declare one or more dependencies. - -*** - -## About Modules - -Swift organizes code into _modules_. Each module specifies a namespace and enforces access controls on which parts of that code can be used outside of that module. - -A program may have all of its code in a single module, or it may import other modules as _dependencies_. Aside from the handful of system-provided modules, such as Darwin on OS X or GLibc on Linux, most dependencies require code to be downloaded and built in order to be used. - -Extracting code that solves a particular problem into a separate module allows for that code to be reused in other situations. For example, a module that provides functionality for making network requests could be shared between a photo sharing app and a program that displays the weather forecast. And if a new module comes along that does a better job, it can be swapped in easily, with minimal change. By embracing modularity, you can focus on the interesting aspects of the problem at hand, rather than getting bogged down solving problems you encounter along the way. - -As a rule of thumb: more modules is probably better than fewer modules. The package manager is designed to make creating both packages and apps with multiple modules as easy as possible. - -### Building Swift Modules - -The Swift Package Manager and its build system needs to understand how to compile your source code. To do this, it uses a convention-based approach which uses the organization of your source code in the file system to determine what you mean, but allows you to fully override and customize these details. A simple example could be: - - foo/Package.swift - foo/Sources/main.swift - -> `Package.swift` is the manifest file that contains metadata about your package. `Package.swift` is documented in a later section. - -If you then run the following command in the directory `foo`: - -```sh -swift build -``` - -Swift will build a single executable called `foo`. - -To the package manager, everything is a package, hence `Package.swift`. However, this does not mean you have to release your software to the wider world; you can develop your app without ever publishing it in a place where others can see or use it. On the other hand, if one day you decide that your project _should_ be available to a wider audience your sources are already in a form ready to be published. The package manager is also independent of specific forms of distribution, so you can use it to share code within your personal projects, within your workgroup, team or company, or with the world. - -Of course, the package manager is used to build itself, so its own source files are laid out following these conventions as well. - -*** - -## About Products - -A target may build either a library or an executable as its product. A library contains a module that can be imported by other Swift code. An executable is a program that can be run by the operating system. - -*** - -## About Dependencies - -Modern development is accelerated by the use of external dependencies (for better and worse). This is great for allowing you to get more done in less time, but adding dependencies to a project has an associated coordination cost. - -In addition to downloading and building the source code for a dependency, that dependency's own dependencies must be downloaded and built as well, and so on, until the entire dependency graph is satisfied. To complicate matters further, a dependency may specify version requirements, which may have to be reconciled with the version requirements of other modules with the same dependency. - -The role of the package manager is to automate the process of downloading and building all of the dependencies for a project, and minimize the coordination costs associated with code reuse. - -Dependencies are specified in your `Package.swift` manifest file. - -### Dependency Hell - -“Dependency Hell” is the colloquialism for a situation where the graph of dependencies required by a project cannot be met. The user is then required to solve the scenario, which is usually a difficult task: - -1. The conflict may be in unfamiliar dependencies (of dependencies) that the user did not explicitly request. -2. Due to the nature of development it would be rare for two dependency graphs to be the same. Thus the amount of help other users (often even the package authors) can offer is limited. Internet searches will likely prove fruitless. - -A good package manager should be designed from the start to minimize the risk of dependency hell, and where this is not possible, to mitigate it and provide tooling so that the user can solve the scenario with a minimum of trouble. The [Package Manager Community Proposal](Design/PackageManagerCommunityProposal.md) contains our thoughts on how we intend to iterate with these hells in mind. - -The following are some of the most common “dependency hell” scenarios: - -* Inappropriate Versioning - A package may specify an inappropriate version for a release. For example, a version is tagged `1.2.3`, but introduces extensive, breaking API changes that should be reflected by a major version bump to `2.0.0`. - -* Incompatible Major Version Requirements - A package may have dependencies with incompatible version requirements for the same package. For example, if `Foo` depends on `Baz` at version `~>1.0` and `Bar` depends on `Baz` at version `~>2.0`, then there is no one version of `Baz` that can satisfy both requirements. This situation often arises when a dependency shared by many packages updates to a new major version, and it takes a long time for all of those packages to update their dependency. - -* Incompatible Minor or Update Version Requirements - A package may have dependencies that are specified too strictly, such that version requirements are incompatible for different minor or update versions. For example, if `Foo` depends on `Baz` at version `==2.0.1` and `Bar` depends on `Baz` at version `==2.0.2`, once again, there is no one version of `Baz` that can satisfy both requirements. This is often the result of a regression introduced in a patch release of a dependency, which causes a package to lock that dependency to a particular version. - -* Namespace Collision - A package may have two or more dependencies that have the same name. For example, a `Person` package depends on an `Addressable` package that defines a protocol for assigning a mailing address to a person, as well as an `Addressable` package that defines a protocol for speaking formally to another person. - -* Broken Software - A package may have a dependency with an outstanding bug that is impacting usability, security, or performance. This may simply be a matter of timeliness on the part of the package maintainers, or a disagreement about their expectations for the package. - -* Global State Conflict - A package may have two or more dependencies that presume to have exclusive access to the same global state. For example, one package may not be able to accommodate another package writing to a particular file path while reading from that same file path. - -* Package Becomes Unavailable - A package may have a dependency on a package that becomes unavailable. This may be caused by the source URL becoming inaccessible, or maintainers deleting a published version. +This directory continues to retain legacy [design notes](Design), [release notes](ReleaseNotes), details about +the [Package Registry API](PackageRegistry), and [libSwiftPM](libSwiftPM). diff --git a/Documentation/ReleaseNotes/5.4.md b/Documentation/ReleaseNotes/5.4.md index 76b0f47e4a5..92f4f22eb25 100644 --- a/Documentation/ReleaseNotes/5.4.md +++ b/Documentation/ReleaseNotes/5.4.md @@ -27,6 +27,6 @@ All [Unicode line terminators](https://www.unicode.org/reports/tr14/) are now re Swift Package Manager now caches package dependency repositories on a per-user basis, which reduces the amount of network traffic and increases performance of dependency resolution for subsequent uses of the same package. -The default location of the cache differs dependening on the platform, but can be controlled using the new `--cache-path` option. SwiftPM also creates a symbolic link at `~/.swiftpm` referencing the default cache location. +The default location of the cache differs depending on the platform, but can be controlled using the new `--cache-path` option. SwiftPM also creates a symbolic link at `~/.swiftpm` referencing the default cache location. Compiled package manifests are also now cached on a per-user basis. This can be overridden using the new `--manifest-cache` option. diff --git a/Documentation/ReleaseNotes/5.6.md b/Documentation/ReleaseNotes/5.6.md index 0d7f6c01c81..23463b7ff28 100644 --- a/Documentation/ReleaseNotes/5.6.md +++ b/Documentation/ReleaseNotes/5.6.md @@ -2,9 +2,9 @@ ### Package Plugins -[SE-0303](https://github.com/apple/swift-evolution/blob/main/proposals/0303-swiftpm-extensible-build-tools.md) Introduces the ability to define build tool plugins in SwiftPM, allowing custom tools to be automatically invoked during a build. Build tool plugins are focused on code generation during the build of a package, for such purposes as generating Swift source files from .proto files or from other inputs, in order to allow build tools to be incorporated into the build graph and to run automatically in a safe manner. +[SE-0303](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0303-swiftpm-extensible-build-tools.md) Introduces the ability to define build tool plugins in SwiftPM, allowing custom tools to be automatically invoked during a build. Build tool plugins are focused on code generation during the build of a package, for such purposes as generating Swift source files from .proto files or from other inputs, in order to allow build tools to be incorporated into the build graph and to run automatically in a safe manner. -[SE-0332](https://github.com/apple/swift-evolution/blob/main/proposals/0332-swiftpm-command-plugins.md) Extends SwiftPM plugin support first introduced with SE-0303 to allow the definition of custom command plugins — plugins that users can invoke directly from the SwiftPM CLI, or from an IDE that supports Swift Packages, in order to perform custom actions on their packages. A command plugin specifies the semantic intent of the command — this might be one of the predefined intents such “documentation generation” or “source code formatting”, or it might be a custom intent with a specialized verb that can be passed to the swift package command. +[SE-0332](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0332-swiftpm-command-plugins.md) Extends SwiftPM plugin support first introduced with SE-0303 to allow the definition of custom command plugins — plugins that users can invoke directly from the SwiftPM CLI, or from an IDE that supports Swift Packages, in order to perform custom actions on their packages. A command plugin specifies the semantic intent of the command — this might be one of the predefined intents such “documentation generation” or “source code formatting”, or it might be a custom intent with a specialized verb that can be passed to the swift package command. ### Manifest API Improvements @@ -26,11 +26,11 @@ Dependency requirement enum calling convention is deprecated in favour of labele ### Other Improvements -Location of configuration files (including mirror file) have changed to accomodate new features that require more robust configuration directories structure, such as SE-0292: +Location of configuration files (including mirror file) have changed to accommodate new features that require more robust configuration directories structure, such as SE-0292: * `/.swiftpm/config` (mirrors file) was moved to `/.swiftpm/configuration/mirrors.json`. SwiftPM 5.6 will automatically copy the file from the old location to the new one and emit a warning to prompt the user to delete the file from the old location. * `~/.swiftpm/config/collections.json` (collections file) was moved to `~/.swiftpm/configuration/collections.json`. SwiftPM 5.6 will automatically copy the file from the old location to the new one and emit a warning to prompt the user to delete the file from the old location. -To increase the security of packages, SwiftPM performs trust on first use (TOFU) validation. The fingerprint of a package is now being recorded when the package is first downloaded from a Git repository or package registry. Subsequent downloads must have fingerpints matching previous recorded values, otherwise it would result in build warnings or failures depending on settings. +To increase the security of packages, SwiftPM performs trust on first use (TOFU) validation. The fingerprint of a package is now being recorded when the package is first downloaded from a Git repository or package registry. Subsequent downloads must have fingerprints matching previous recorded values, otherwise it would result in build warnings or failures depending on settings. Introduce a second version of `Package.resolved` file format which more accurately captures package identity. diff --git a/Documentation/ReleaseNotes/5.7.md b/Documentation/ReleaseNotes/5.7.md index 4543939a39c..44edbf6e303 100644 --- a/Documentation/ReleaseNotes/5.7.md +++ b/Documentation/ReleaseNotes/5.7.md @@ -2,17 +2,17 @@ ### Package Plugins -[SE-0303: Build tool plugins](https://github.com/apple/swift-evolution/blob/main/proposals/0303-swiftpm-extensible-build-tools.md) and [SE-0332: Command Plugins](https://github.com/apple/swift-evolution/blob/main/proposals/0332-swiftpm-command-plugins.md) which were first introduced in Swift 5.6 have been further refined, with support for generating resources and improved diagnostics. To learn more, refer to the [Getting Started with Plugins](../Plugins.md) guide. +[SE-0303: Build tool plugins](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0303-swiftpm-extensible-build-tools.md) and [SE-0332: Command Plugins](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0332-swiftpm-command-plugins.md) which were first introduced in Swift 5.6 have been further refined, with support for generating resources and improved diagnostics. To learn more, refer to the [Getting Started with Plugins](../Plugins.md) guide. ### Package Registry Support -SwiftPM now supports package registry related capabilities introduced by [SE-0292](https://github.com/apple/swift-evolution/blob/main/proposals/0292-package-registry-service.md) and the corresponding [service specification](../Registry.md). With the exception of package publishing, SwiftPM can resolve and download dependencies from any compliant registry using the defined APIs. +SwiftPM now supports package registry related capabilities introduced by [SE-0292](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0292-package-registry-service.md) and the corresponding [service specification](../Registry.md). With the exception of package publishing, SwiftPM can resolve and download dependencies from any compliant registry using the defined APIs. To get started, users will need to specify their package registry by running the `swift package-registry set` subcommand or editing the `registries.json` configuration file. `swift package` tool's `--use-registry-identity-for-scm` and `--replace-scm-with-registry` options might also be of interest. ### Module Aliasing For Disambiguation -Modules with the same name from different packages can now be disambiguated by module aliasing [SE-0339](https://github.com/apple/swift-evolution/blob/main/proposals/0339-module-aliasing-for-disambiguation.md). When adding a product dependency for a target in a package manifest, use a new parameter `moduleAliases` to provide a new unique name for a conflicting module. +Modules with the same name from different packages can now be disambiguated by module aliasing [SE-0339](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0339-module-aliasing-for-disambiguation.md). When adding a product dependency for a target in a package manifest, use a new parameter `moduleAliases` to provide a new unique name for a conflicting module. * [#4119] diff --git a/Documentation/ReleaseNotes/5.8.md b/Documentation/ReleaseNotes/5.8.md index 71f4de43424..042f40d44df 100644 --- a/Documentation/ReleaseNotes/5.8.md +++ b/Documentation/ReleaseNotes/5.8.md @@ -2,7 +2,7 @@ ## Package manifest changes -SwiftPM targets can now specify the upcoming language features they require. `Package.swift` manifest syntax has been expanded with an API to include setting `enableUpcomingFeature` and `enableExperimentalFeature` flags at the target level, as specified by [SE-0362](https://github.com/apple/swift-evolution/blob/main/proposals/0362-piecemeal-future-features.md). +SwiftPM targets can now specify the upcoming language features they require. `Package.swift` manifest syntax has been expanded with an API to include setting `enableUpcomingFeature` and `enableExperimentalFeature` flags at the target level, as specified by [SE-0362](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0362-piecemeal-future-features.md). SwiftPM now allows exposing an executable product that consists solely of a binary target that is backed by an artifact bundle. This allows vending binary executables as their own separate package, independently of plugins that are using them. @@ -10,9 +10,9 @@ In packages using tools version 5.8 or later, `Foundation` is no longer implicit ## Package Registry Support -SwiftPM now supports token authentication when interacting with a package registry. The `swift package-registry` command has two new subcommands `login` and `logout` as defined in [SE-0378](https://github.com/apple/swift-evolution/blob/main/proposals/0378-package-registry-auth.md) for adding/removing registry credentials. +SwiftPM now supports token authentication when interacting with a package registry. The `swift package-registry` command has two new subcommands `login` and `logout` as defined in [SE-0378](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0378-package-registry-auth.md) for adding/removing registry credentials. -## Other improvments +## Other improvements Improved handling of offline behavior when a compatible cached version of a dependency exists on disk in either the per-package or shared cache. SwiftPM will check for network availability status to determine if it should attempt to update a checked version of a dependency, and when offline will use the cached version without an update. diff --git a/Documentation/Usage.md b/Documentation/Usage.md deleted file mode 100644 index 24eeee9057c..00000000000 --- a/Documentation/Usage.md +++ /dev/null @@ -1,849 +0,0 @@ -# Usage - -## Table of Contents - -* [Overview](README.md) -* [**Usage**](Usage.md) - * [Creating a Package](#creating-a-package) - * [Creating a Library Package](#creating-a-library-package) - * [Creating an Executable Package](#creating-an-executable-package) - * [Creating a Macro Package](#creating-a-macro-package) - * [Defining Dependencies](#defining-dependencies) - * [Publishing a Package](#publishing-a-package) - * [Requiring System Libraries](#requiring-system-libraries) - * [Packaging Legacy Code](#packaging-legacy-code) - * [Handling Version-specific Logic](#handling-version-specific-logic) - * [Editing a Package](#editing-a-package) - * [Top of Tree Development](#top-of-tree-development) - * [Resolving Versions (Package.resolved file)](#resolving-versions-packageresolved-file) - * [Setting the Swift Tools Version](#setting-the-swift-tools-version) - * [Testing](#testing) - * [Running](#running) - * [Setting the Build Configuration](#setting-the-build-configuration) - * [Debug](#debug) - * [Release](#release) - * [Additional Flags](#additional-flags) - * [Depending on Apple Modules](#depending-on-apple-modules) - * [Creating C Language Targets](#creating-c-language-targets) - * [Using Shell Completion Scripts](#using-shell-completion-scripts) - * [Package manifest specification](PackageDescription.md) - * [Packages and continuous integration](ContinousIntegration.md) - ---- - -## Creating a Package - -Simply put: a package is a git repository with semantically versioned tags, -that contains Swift sources and a `Package.swift` manifest file at its root. - -### Creating a Library Package - -A library package contains code which other packages can use and depend on. To -get started, create a directory and run `swift package init`: - - $ mkdir MyPackage - $ cd MyPackage - $ swift package init # or swift package init --type library - $ swift build - $ swift test - -This will create the directory structure needed for a library package with a -target and the corresponding test target to write unit tests. A library package -can contain multiple targets as explained in [Target Format -Reference](PackageDescription.md#target). - -### Creating an Executable Package - -SwiftPM can create native binaries which can be executed from the command line. To -get started: - - $ mkdir MyExecutable - $ cd MyExecutable - $ swift package init --type executable - $ swift build - $ swift run - Hello, World! - -This creates the directory structure needed for executable targets. Any target -can be turned into a executable target if there is a `main.swift` file present in -its sources. The complete reference for layout is -[here](PackageDescription.md#target). - -### Creating a Macro Package - -SwiftPM can generate boilerplate for custom macros: - - $ mkdir MyMacro - $ cd MyMacro - $ swift package init --type macro - $ swift build - $ swift run - The value 42 was produced by the code "a + b" - -This creates a package with a `.macro` type target with its required dependencies -on [swift-syntax](https://github.com/apple/swift-syntax), a library `.target` -containing the macro's code, and an `.executableTarget` and `.testTarget` for -running the macro. The sample macro, `StringifyMacro`, is documented in the Swift -Evolution proposal for [Expression Macros](https://github.com/apple/swift-evolution/blob/main/proposals/0382-expression-macros.md) -and the WWDC [Write Swift macros](https://developer.apple.com/videos/play/wwdc2023/10166) -video. See further documentation on macros in [The Swift Programming Language](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/macros/) book. - -## Defining Dependencies - -To depend on a package, define the dependency and the version in the manifest of -your package, and add a product from that package as a dependency, e.g., if -you want to use https://github.com/apple/example-package-playingcard as -a dependency, add the GitHub URL in the dependencies of `Package.swift`: - -```swift -import PackageDescription - -let package = Package( - name: "MyPackage", - dependencies: [ - .package(url: "https://github.com/apple/example-package-playingcard.git", from: "3.0.4"), - ], - targets: [ - .target( - name: "MyPackage", - dependencies: ["PlayingCard"] - ), - .testTarget( - name: "MyPackageTests", - dependencies: ["MyPackage"] - ), - ] -) -``` - -Now you should be able to `import PlayingCard` in the `MyPackage` target. - -## Publishing a Package - -To publish a package, create and push a semantic version tag: - - $ git init - $ git add . - $ git remote add origin [github-URL] - $ git commit -m "Initial Commit" - $ git tag 1.0.0 - $ git push origin master --tags - -Now other packages can depend on version 1.0.0 of this package using the github -url. -An example of a published package can be found here: -https://github.com/apple/example-package-fisheryates - -## Requiring System Libraries - -You can link against system libraries using the package manager. To do so, you'll -need to add a special `target` of type `.systemLibrary`, and a `module.modulemap` -for each system library you're using. - -Let's see an example of adding [libgit2](https://libgit2.github.com) as a -dependency to an executable target. - -Create a directory called `example`, and initialize it as a package that -builds an executable: - - $ mkdir example - $ cd example - example$ swift package init --type executable - -Edit the `Sources/example/main.swift` so it consists of this code: - -```swift -import Clibgit - -let options = git_repository_init_options() -print(options) -``` - -To `import Clibgit`, the package manager requires that the libgit2 library has -been installed by a system packager (eg. `apt`, `brew`, `yum`, `nuget`, etc.). The -following files from the libgit2 system-package are of interest: - - /usr/local/lib/libgit2.dylib # .so on Linux - /usr/local/include/git2.h - -**Note:** the system library may be located elsewhere on your system, such as: -- `/usr/`, or `/opt/homebrew/` if you're using Homebrew on an Apple Silicon Mac. -- `C:\vcpkg\installed\x64-windows\include` on Windows, if you're using `vcpkg`. -On most Unix-like systems, you can use `pkg-config` to lookup where a library is installed: - - example$ pkg-config --cflags libgit2 - -I/usr/local/libgit2/1.6.4/include - - -**First, let's define the `target` in the package description**: - -```swift -// swift-tools-version: 5.8 -// The swift-tools-version declares the minimum version of Swift required to build this package. - -import PackageDescription - -let package = Package( - name: "example", - targets: [ - // systemLibrary is a special type of build target that wraps a system library - // in a target that other targets can require as their depencency. - .systemLibrary( - name: "Clibgit", - pkgConfig: "libgit2", - providers: [ - .brew(["libgit2"]), - .apt(["libgit2-dev"]) - ] - ) - ] -) - -``` - -**Note:** For Windows-only packages `pkgConfig` should be omitted as -`pkg-config` is not expected to be available. If you don't want to use the -`pkgConfig` parameter you can pass the path of a directory containing the -library using the `-L` flag in the command line when building your package -instead. - - example$ swift build -Xlinker -L/usr/local/lib/ - -Next, create a directory `Sources/Clibgit` in your `example` project, and -add a `module.modulemap` and the header file to it: - - module Clibgit [system] { - header "git2.h" - link "git2" - export * - } - -The header file should look like this: - -```c -// git2.h -#pragma once -#include -``` - -**Note:** Avoiding specifying an absolute path to `git2.h` provided -by the library in the `module.modulemap`. Doing so will break compatibility of -your project between machines that may use a different file system layout or -install libraries to different paths. - -> The convention we hope the community will adopt is to prefix such modules -> with `C` and to camelcase the modules as per Swift module name conventions. -> Then the community is free to name another module simply `libgit` which -> contains more “Swifty” function wrappers around the raw C interface. - -The `example` directory structure should look like this now: - - . - ├── Package.swift - └── Sources - ├── Clibgit - │   ├── git2.h - │   └── module.modulemap - └── main.swift - -At this point, your system library target is fully defined, and you can now use -that target as a dependency in other targets in your `Package.swift`, like this: - -```swift - -import PackageDescription - -let package = Package( - name: "example", - targets: [ - .executableTarget( - name: "example", - - // example executable requires "Clibgit" target as its dependency. - // It's a systemLibrary target defined below. - dependencies: ["Clibgit"], - path: "Sources" - ), - - // systemLibrary is a special type of build target that wraps a system library - // in a target that other targets can require as their depencency. - .systemLibrary( - name: "Clibgit", - pkgConfig: "libgit2", - providers: [ - .brew(["libgit2"]), - .apt(["libgit2-dev"]) - ] - ) - ] -) - -``` - -Now if we type `swift build` in our example app directory we will create an -executable: - - example$ swift build - … - example$ .build/debug/example - git_repository_init_options(version: 0, flags: 0, mode: 0, workdir_path: nil, description: nil, template_path: nil, initial_head: nil, origin_url: nil) - example$ - -### Requiring a System Library Without `pkg-config` - -Let’s see another example of using [IJG’s JPEG library](http://www.ijg.org) -from an executable, which has some caveats. - -Create a directory called `example`, and initialize it as a package that builds -an executable: - - $ mkdir example - $ cd example - example$ swift package init --type executable - -Edit the `Sources/main.swift` so it consists of this code: - -```swift -import CJPEG - -let jpegData = jpeg_common_struct() -print(jpegData) -``` - -Install the JPEG library, on macOS you can use Homebrew package manager: `brew install jpeg`. -`jpeg` is a keg-only formula, meaning it won't be linked to `/usr/local/lib`, -and you'll have to link it manually at build time. - -Just like in the previous example, run `mkdir Sources/CJPEG` and add the -following `module.modulemap`: - - module CJPEG [system] { - header "shim.h" - header "/usr/local/opt/jpeg/include/jpeglib.h" - link "jpeg" - export * - } - -Create a `shim.h` file in the same directory and add `#include ` in -it. - - $ echo '#include ' > shim.h - -This is because `jpeglib.h` is not a correct module, that is, it does not contain -the required line `#include `. Alternatively, you can add `#include ` -to the top of jpeglib.h to avoid creating the `shim.h` file. - -Now to use the CJPEG package we must declare our dependency in our example -app’s `Package.swift`: - -```swift - -import PackageDescription - -let package = Package( - name: "example", - targets: [ - .executableTarget( - name: "example", - dependencies: ["CJPEG"], - path: "Sources" - ), - .systemLibrary( - name: "CJPEG", - providers: [ - .brew(["jpeg"]) - ]) - ] -) -``` - -Now if we type `swift build` in our example app directory we will create an -executable: - - example$ swift build -Xlinker -L/usr/local/jpeg/lib - … - example$ .build/debug/example - jpeg_common_struct(err: nil, mem: nil, progress: nil, client_data: nil, is_decompressor: 0, global_state: 0) - example$ - -We have to specify the path where the libjpeg is present using `-Xlinker` because -there is no pkg-config file for it. We plan to provide a solution to avoid passing -the flag in the command line. - -### Packages That Provide Multiple Libraries - -Some system packages provide multiple libraries (`.so` and `.dylib` files). In -such cases you should add all the libraries to that Swift modulemap package’s -`.modulemap` file: - - module CFoo [system] { - header "/usr/local/include/foo/foo.h" - link "foo" - export * - } - - module CFooBar [system] { - header "/usr/include/foo/bar.h" - link "foobar" - export * - } - - module CFooBaz [system] { - header "/usr/include/foo/baz.h" - link "foobaz" - export * - } - -`foobar` and `foobaz` link to `foo`; we don’t need to specify this information -in the module-map because the headers `foo/bar.h` and `foo/baz.h` both include -`foo/foo.h`. It is very important however that those headers do include their -dependent headers, otherwise when the modules are imported into Swift the -dependent modules will not get imported automatically and link errors will -happen. If these link errors occur for consumers of a package that consumes your -package, the link errors can be especially difficult to debug. - -### Cross-platform Module Maps - -Module maps must contain absolute paths, thus they are not cross-platform. We -intend to provide a solution for this in the package manager. In the long term, -we hope that system libraries and system packagers will provide module maps and -thus this component of the package manager will become redundant. - -*Notably* the above steps will not work if you installed JPEG and JasPer with -[Homebrew](http://brew.sh) since the files will be installed to `/usr/local` on -Intel Macs, or /opt/homebrew on Apple silicon Macs. For now adapt the paths, -but as said, we plan to support basic relocations like these. - -### Module Map Versioning - -Version the module maps semantically. The meaning of semantic version is less -clear here, so use your best judgement. Do not follow the version of the system -library the module map represents; version the module map(s) independently. - -Follow the conventions of system packagers; for example, the debian package for -python3 is called python3, as there is not a single package for python and -python is designed to be installed side-by-side. Were you to make a module map -for python3 you should name it `CPython3`. - -### System Libraries With Optional Dependencies - -At this time you will need to make another module map package to represent -system packages that are built with optional dependencies. - -For example, `libarchive` optionally depends on `xz`, which means it can be -compiled with `xz` support, but it is not required. To provide a package that -uses libarchive with xz you must make a `CArchive+CXz` package that depends on -`CXz` and provides `CArchive`. - -## Packaging Legacy Code - -You may be working with code that builds both as a package and not. For example, -you may be packaging a project that also builds with Xcode. - -In these cases, you can use the preprocessor definition `SWIFT_PACKAGE` to -conditionally compile code for Swift packages. - -In your source file: -```swift -#if SWIFT_PACKAGE -import Foundation -#endif -``` - -## Handling Version-specific Logic - -The package manager is designed to support packages which work with a variety of -Swift project versions, including both the language and the package manager -version. - -In most cases, if you want to support multiple Swift versions in a package you -should do so by using the language-specific version checks available in the -source code itself. However, in some circumstances this may become unmanageable, -specifically, when the package manifest itself cannot be written to be Swift -version agnostic (for example, because it optionally adopts new package manager -features not present in older versions). - -The package manager has support for a mechanism to allow Swift version-specific -customizations for the both package manifest and the package versions which will -be considered. - -### Version-specific Tag Selection - -The tags which define the versions of the package available for clients to use -can _optionally_ be suffixed with a marker in the form of `@swift-3`. When the -package manager is determining the available tags for a repository, _if_ -a version-specific marker is available which matches the current tool version, -then it will *only* consider the versions which have the version-specific -marker. Conversely, version-specific tags will be ignored by any non-matching -tool version. - -For example, suppose the package `Foo` has the tags `[1.0.0, 1.2.0@swift-3, -1.3.0]`. If version 3.0 of the package manager is evaluating the available -versions for this repository, it will only ever consider version `1.2.0`. -However, version 4.0 would consider only `1.0.0` and `1.3.0`. - -This feature is intended for use in the following scenarios: - -1. A package wishes to maintain support for Swift 3.0 in older versions, but - newer versions of the package require Swift 4.0 for the manifest to be - readable. Since Swift 3.0 will not know to ignore those versions, it would - fail when performing dependency resolution on the package if no action is - taken. In this case, the author can re-tag the last versions which supported - Swift 3.0 appropriately. - -2. A package wishes to maintain dual support for Swift 3.0 and Swift 4.0 at the - same version numbers, but this requires substantial differences in the code. - In this case, the author can maintain parallel tag sets for both versions. - -It is *not* expected that the packages would ever use this feature unless absolutely -necessary to support existing clients. Specifically, packages *should not* -adopt this syntax for tagging versions supporting the _latest GM_ Swift -version. - -The package manager supports looking for any of the following marked tags, in -order of preference: - -1. `MAJOR.MINOR.PATCH` (e.g., `1.2.0@swift-3.1.2`) -2. `MAJOR.MINOR` (e.g., `1.2.0@swift-3.1`) -3. `MAJOR` (e.g., `1.2.0@swift-3`) - -### Version-specific Manifest Selection - -The package manager will additionally look for a version-specific marked -manifest version when loading the particular version of a package, by searching -for a manifest in the form of `Package@swift-3.swift`. The set of markers -looked for is the same as for version-specific tag selection. - -This feature is intended for use in cases where a package wishes to maintain -compatibility with multiple Swift project versions, but requires a -substantively different manifest file for this to be viable (e.g., due to -changes in the manifest API). - -It is *not* expected the packages would ever use this feature unless absolutely -necessary to support existing clients. Specifically, packages *should not* -adopt this syntax for tagging versions supporting the _latest GM_ Swift -version. - -In case the current Swift version doesn't match any version-specific manifest, -the package manager will pick the manifest with the most compatible tools -version. For example, if there are three manifests: - -`Package.swift` (tools version 3.0) -`Package@swift-4.swift` (tools version 4.0) -`Package@swift-4.2.swift` (tools version 4.2) - -The package manager will pick `Package.swift` on Swift 3, `Package@swift-4.swift` on -Swift 4, and `Package@swift-4.2.swift` on Swift 4.2 and above because its tools -version will be most compatible with future version of the package manager. - -## Editing a Package - -Swift package manager supports editing dependencies, when your work requires -making a change to one of your dependencies (for example, to fix a bug, or add -a new API). The package manager moves the dependency into a location under the -`Packages/` directory where it can be edited. - -For the packages which are in the editable state, `swift build` will always use -the exact sources in this directory to build, regardless of their state, Git -repository status, tags, or the tag desired by dependency resolution. In other -words, this will _just build_ against the sources that are present. When an -editable package is present, it will be used to satisfy all instances of that -package in the dependency graph. It is possible to edit all, some, or none of -the packages in a dependency graph, without restriction. - -Editable packages are best used to do experimentation with dependency code, or to -create and submit a patch in the dependency owner's repository (upstream). -There are two ways to put a package in editable state: - - $ swift package edit Foo --branch bugFix - -This will create a branch called `bugFix` from the currently resolved version and -put the dependency `Foo` in the `Packages/` directory. - - $ swift package edit Foo --revision 969c6a9 - -This is similar to the previous version, except that the Package Manager will leave -the dependency at a detached HEAD on the specified revision. - -Note: If the branch or revision option is not provided, the Package Manager will -checkout the currently resolved version on a detached HEAD. - -Once a package is in an editable state, you can navigate to the directory -`Packages/Foo` to make changes, build and then push the changes or open a pull -request to the upstream repository. - -You can end editing a package using `unedit` command: - - $ swift package unedit Foo - -This will remove the edited dependency from `Packages/` and put the originally -resolved version back. - -This command fails if there are uncommited changes or changes which are not -pushed to the remote repository. If you want to discard these changes and -unedit, you can use the `--force` option: - - $ swift package unedit Foo --force - -### Top of Tree Development - -This feature allows overriding a dependency with a local checkout on the -filesystem. This checkout is completely unmanaged by the package manager and -will be used as-is. The only requirement is that the package name in the -overridden checkout should not change. This is extremely useful when developing -multiple packages in tandem or when working on packages alongside an -application. - -The command to attach (or create) a local checkout is: - - $ swift package edit --path - -For example, if `Foo` depends on `Bar` and you have a checkout of `Bar` at -`/workspace/bar`: - - foo$ swift package edit Bar --path /workspace/bar - -A checkout of `Bar` will be created if it doesn't exist at the given path. If -a checkout exists, package manager will validate the package name at the given -path and attach to it. - -The package manager will also create a symlink in the `Packages/` directory to the -checkout path. - -Use unedit command to stop using the local checkout: - - $ swift package unedit - # Example: - $ swift package unedit Bar - -## Resolving Versions (Package.resolved File) - -The package manager records the result of dependency resolution in a -`Package.resolved` file in the top-level of the package, and when this file is -already present in the top-level, it is used when performing dependency -resolution, rather than the package manager finding the latest eligible version -of each package. Running `swift package update` updates all dependencies to the -latest eligible versions and updates the `Package.resolved` file accordingly. - -Resolved versions will always be recorded by the package manager. Some users may -choose to add the Package.resolved file to their package's .gitignore file. When -this file is checked in, it allows a team to coordinate on what versions of the -dependencies they should use. If this file is gitignored, each user will -separately choose when to get new versions based on when they run the `swift -package update` command, and new users will start with the latest eligible -version of each dependency. Either way, for a package which is a dependency of -other packages (e.g., a library package), that package's `Package.resolved` file -will not have any effect on its client packages. - -The `swift package resolve` command resolves the dependencies, taking into -account the current version restrictions in the `Package.swift` manifest and -`Package.resolved` resolved versions file, and issuing an error if the graph -cannot be resolved. For packages which have previously resolved versions -recorded in the `Package.resolved` file, the resolve command will resolve to -those versions as long as they are still eligible. If the resolved version's file -changes (e.g., because a teammate pushed a new version of the file) the next -resolve command will update packages to match that file. After a successful -resolve command, the checked out versions of all dependencies and the versions -recorded in the resolved versions file will match. In most cases the resolve -command will perform no changes unless the `Package.swift` manifest or -`Package.resolved` file have changed. - -Most SwiftPM commands will implicitly invoke the `swift package resolve` -functionality before running, and will cancel with an error if dependencies -cannot be resolved. - -## Setting the Swift Tools Version - -The tools version declares the minimum version of the Swift tools required to -use the package, determines what version of the PackageDescription API should -be used in the `Package.swift` manifest, and determines which Swift language -compatibility version should be used to parse the `Package.swift` manifest. - -When resolving package dependencies, if the version of a dependency that would -normally be chosen specifies a Swift tools version which is greater than the -version in use, that version of the dependency will be considered ineligible -and dependency resolution will continue with evaluating the next-best version. -If no version of a dependency (which otherwise meets the version requirements -from the package dependency graph) supports the version of the Swift tools in -use, a dependency resolution error will result. - -### Swift Tools Version Specification - -The Swift tools version is specified by a special comment in the first line of -the `Package.swift` manifest. To specify a tools version, a `Package.swift` file -must begin with the string `// swift-tools-version:`, followed by a version -number specifier. - -The version number specifier follows the syntax defined by semantic versioning -2.0, with an amendment that the patch version component is optional and -considered to be 0 if not specified. The `semver` syntax allows for an optional -pre-release version component or build version component; those components will -be completely ignored by the package manager currently. -After the version number specifier, an optional `;` character may be present; -it, and anything else after it until the end of the first line, will be ignored -by this version of the package manager, but is reserved for the use of future -versions of the package manager. - -Some Examples: - - // swift-tools-version:3.1 - // swift-tools-version:3.0.2 - // swift-tools-version:4.0 - -### Tools Version Commands - -The following Swift tools version commands are supported: - -* Report tools version of the package: - - $ swift package tools-version - -* Set the package's tools version to the version of the tools currently in use: - - $ swift package tools-version --set-current - -* Set the tools version to a given value: - - $ swift package tools-version --set - -## Testing - -Use the `swift test` tool to run the tests of a Swift package. For more information on -the test tool, run `swift test --help`. - -## Running - -Use the `swift run [executable [arguments...]]` tool to run an executable product of a Swift -package. The executable's name is optional when running without arguments and when there -is only one executable product. For more information on the run tool, run -`swift run --help`. - -## Setting the Build Configuration - -SwiftPM allows two build configurations: Debug (default) and Release. - -### Debug - -By default, running `swift build` will build in its debug configuration. -Alternatively, you can also use `swift build -c debug`. The build artifacts are -located in a directory called `debug` under the build folder. A Swift target is built -with the following flags in debug mode: - -* `-Onone`: Compile without any optimization. -* `-g`: Generate debug information. -* `-enable-testing`: Enable the Swift compiler's testability feature. - -A C language target is built with the following flags in debug mode: - -* `-O0`: Compile without any optimization. -* `-g`: Generate debug information. - -### Release - -To build in release mode, type `swift build -c release`. The build artifacts -are located in directory named `release` under the build folder. A Swift target is -built with following flags in release mode: - -* `-O`: Compile with optimizations. -* `-whole-module-optimization`: Optimize input files (per module) together - instead of individually. - -A C language target is built with following flags in release mode: - -* `-O2`: Compile with optimizations. - -### Additional Flags - -You can pass more flags to the C, C++, or Swift compilers in three different ways: - -* Command-line flags passed to these tools: flags like `-Xcc` or `-Xswiftc` are used to - pass C or Swift flags to all targets, as shown with `-Xlinker` above. -* Target-specific flags in the manifest: options like `cSettings` or `swiftSettings` are - used for fine-grained control of compilation flags for particular targets. -* A destination JSON file: once you have a set of working command-line flags that you - want applied to all targets, you can collect them in a JSON file and pass them in through - `extra-cc-flags` and `extra-swiftc-flags` with `--destination example.json`. Take a - look at `Utilities/build_ubuntu_cross_compilation_toolchain` for an example. - -One difference is that C flags passed in the `-Xcc` command-line or manifest's `cSettings` -are supplied to the Swift compiler too for convenience, but `extra-cc-flags` aren't. - -## Depending on Apple Modules - -Swift Package Manager includes a build system that can build for macOS and Linux. -Xcode 11 integrates with `libSwiftPM` to provide support for iOS, watchOS, and tvOS platforms. -To build your package with Xcode from command line you can use -[`xcodebuild`](https://developer.apple.com/library/archive/technotes/tn2339/_index.html). -An example invocation would be: - -``` -xcodebuild -scheme Foo -destination 'generic/platform=iOS' -``` - -where `Foo` would be the name of the library product you're trying to build. You can -get the full list of available schemes for you SwiftPM package with `xcodebuild -list`. -You can get the list of available destinations for a given scheme with this invocation: - -``` -xcodebuild -showdestinations -scheme Foo -``` - - -## Creating C Language Targets - -C language targets are similar to Swift targets, except that the C language -libraries should contain a directory named `include` to hold the public headers. - -To allow a Swift target to import a C language target, add a [target](PackageDescription.md#target) in the manifest file. Swift Package Manager will -automatically generate a modulemap for each C language library target for these -3 cases: - -* If `include/Foo/Foo.h` exists and `Foo` is the only directory under the - include directory, and the include directory contains no header files, then - `include/Foo/Foo.h` becomes the umbrella header. - -* If `include/Foo.h` exists and `include` contains no other subdirectory, then - `include/Foo.h` becomes the umbrella header. - -* Otherwise, the `include` directory becomes an umbrella directory, which means - that all headers under it will be included in the module. - -In case of complicated `include` layouts or headers that are not compatible with -modules, a custom `module.modulemap` can be provided in the `include` directory. - -For executable targets, only one valid C language main file is allowed, e.g., it -is invalid to have `main.c` and `main.cpp` in the same target. - -## Using Shell Completion Scripts - -SwiftPM ships with completion scripts for both Bash and ZSH. These files should be generated in order to use them. - -### Bash - -Use the following commands to install the Bash completions to `~/.swift-package-complete.bash` and automatically load them using your `~/.bash_profile` file. - -```bash -swift package completion-tool generate-bash-script > ~/.swift-package-complete.bash -echo -e "source ~/.swift-package-complete.bash\n" >> ~/.bash_profile -source ~/.swift-package-complete.bash -``` - -Alternatively, add the following commands to your `~/.bash_profile` file to directly load completions: - -```bash -# Source Swift completion -if [ -n "`which swift`" ]; then - eval "`swift package completion-tool generate-bash-script`" -fi -``` - -### ZSH - -Use the following commands to install the ZSH completions to `~/.zsh/_swift`. You can chose a different folder, but the filename should be `_swift`. This will also add `~/.zsh` to your `$fpath` using your `~/.zshrc` file. - -```bash -mkdir ~/.zsh -swift package completion-tool generate-zsh-script > ~/.zsh/_swift -echo -e "fpath=(~/.zsh \$fpath)\n" >> ~/.zshrc -compinit -``` diff --git a/Documentation/libSwiftPM.md b/Documentation/libSwiftPM.md index 144f8bec18b..7ea9c64621e 100644 --- a/Documentation/libSwiftPM.md +++ b/Documentation/libSwiftPM.md @@ -10,12 +10,12 @@ A subset of `libSwiftPM` that includes only the data model (without SwiftPM's build system) is available as `libSwiftPMDataModel`. Any one client should depend on one or the other, but not both. -The SwiftPM repository contains an [example](https://github.com/apple/swift-package-manager/tree/master/Examples/package-info) that demonstrates the use of +The SwiftPM repository contains an [example](https://github.com/swiftlang/swift-package-manager/tree/master/Examples/package-info) that demonstrates the use of `libSwiftPM` in a Swift package. Use the following commands to run the example package: ```sh -$ git clone https://github.com/apple/swift-package-manager +$ git clone https://github.com/swiftlang/swift-package-manager $ cd swift-package-manager/Examples/package-info $ swift run ``` diff --git a/Examples/package-info/Package.swift b/Examples/package-info/Package.swift index 94e5d6d6ffa..3fcc7509b5a 100644 --- a/Examples/package-info/Package.swift +++ b/Examples/package-info/Package.swift @@ -1,18 +1,18 @@ -// swift-tools-version:5.5 +// swift-tools-version:5.7 import PackageDescription let package = Package( name: "package-info", platforms: [ - .macOS(.v12), + .macOS(.v13), .iOS(.v13) ], dependencies: [ // This just points to the SwiftPM at the root of this repository. .package(name: "swift-package-manager", path: "../../"), // You will want to depend on a stable semantic version instead: - // .package(url: "https://github.com/apple/swift-package-manager", .exact("0.4.0")) + // .package(url: "https://github.com/swiftlang/swift-package-manager", .exact("0.4.0")) ], targets: [ .executableTarget( diff --git a/Examples/package-info/Sources/package-info/example.swift b/Examples/package-info/Sources/package-info/example.swift index 0ec45d4ecf5..579a5331e25 100644 --- a/Examples/package-info/Sources/package-info/example.swift +++ b/Examples/package-info/Sources/package-info/example.swift @@ -26,8 +26,8 @@ struct Example { let package = try await workspace.loadRootPackage(at: packagePath, observabilityScope: observability.topScope) - let graph = try workspace.loadPackageGraph(rootPath: packagePath, observabilityScope: observability.topScope) - + let graph = try await workspace.loadPackageGraph(rootPath: packagePath, observabilityScope: observability.topScope) + // EXAMPLES // ======== @@ -39,11 +39,11 @@ struct Example { print("Targets:", targets) // Package - let executables = package.targets.filter({ $0.type == .executable }).map({ $0.name }) + let executables = package.modules.filter({ $0.type == .executable }).map({ $0.name }) print("Executable targets:", executables) // PackageGraph - let numberOfFiles = graph.reachableTargets.reduce(0, { $0 + $1.sources.paths.count }) + let numberOfFiles = graph.reachableModules.reduce(0, { $0 + $1.sources.paths.count }) print("Total number of source files (including dependencies):", numberOfFiles) } } diff --git a/Fixtures/BinaryLibraries/Static/Package1/Package.swift b/Fixtures/BinaryLibraries/Static/Package1/Package.swift new file mode 100644 index 00000000000..f169ded8ec7 --- /dev/null +++ b/Fixtures/BinaryLibraries/Static/Package1/Package.swift @@ -0,0 +1,26 @@ +// swift-tools-version: 6.0 + +import PackageDescription + +let package = Package( + name: "Package1", + targets: [ + .executableTarget( + name: "Example", + dependencies: [ + "Simple", + "Wrapper" + ] + ), + .target( + name: "Wrapper", + dependencies: [ + "Simple" + ] + ), + .binaryTarget( + name: "Simple", + path: "Simple.artifactbundle" + ), + ] +) diff --git a/Fixtures/BinaryLibraries/Static/Package1/Simple.artifactbundle/Makefile b/Fixtures/BinaryLibraries/Static/Package1/Simple.artifactbundle/Makefile new file mode 100644 index 00000000000..2eee0071e9f --- /dev/null +++ b/Fixtures/BinaryLibraries/Static/Package1/Simple.artifactbundle/Makefile @@ -0,0 +1,87 @@ +SRC_FILES := $(wildcard *.c) + +# Define architectures and platforms +ARCHS := x86_64 arm64 +PLATFORMS := macos linux + +# Define output directories +BUILD_DIR := build +DIST_DIR := dist + +# Platform-specific settings +MACOS_SDK := $(shell xcrun --sdk macosx --show-sdk-path 2>/dev/null || echo "") +MACOS_MIN_VERSION := 10.15 + +# Compiler flags +COMMON_FLAGS := -O2 + +# Platform and architecture specific flags +MACOS_X86_64_FLAGS := -target x86_64-apple-macos$(MACOS_MIN_VERSION) -isysroot $(MACOS_SDK) +MACOS_ARM64_FLAGS := -target arm64-apple-macos$(MACOS_MIN_VERSION) -isysroot $(MACOS_SDK) +LINUX_X86_64_FLAGS := -target x86_64-unknown-linux-gnu +LINUX_ARM64_FLAGS := -target aarch64-unknown-linux-gnu + +.PHONY: all clean macos linux universal + +all: macos linux + +# Create necessary directories +$(BUILD_DIR): + mkdir -p $(BUILD_DIR)/macos/x86_64 + mkdir -p $(BUILD_DIR)/macos/arm64 + mkdir -p $(BUILD_DIR)/linux/x86_64 + mkdir -p $(BUILD_DIR)/linux/arm64 + +$(DIST_DIR): + mkdir -p $(DIST_DIR)/macos + mkdir -p $(DIST_DIR)/linux + +# macOS x86_64 build +$(BUILD_DIR)/macos/x86_64/%.o: %.c | $(BUILD_DIR) + clang $(COMMON_FLAGS) $(MACOS_X86_64_FLAGS) -c -o $@ $< + +# macOS arm64 build +$(BUILD_DIR)/macos/arm64/%.o: %.c | $(BUILD_DIR) + clang $(COMMON_FLAGS) $(MACOS_ARM64_FLAGS) -c -o $@ $< + +# Linux x86_64 build +$(BUILD_DIR)/linux/x86_64/%.o: %.c | $(BUILD_DIR) + clang $(COMMON_FLAGS) $(LINUX_X86_64_FLAGS) -c -o $@ $< + +# Linux arm64 build +$(BUILD_DIR)/linux/arm64/%.o: %.c | $(BUILD_DIR) + clang $(COMMON_FLAGS) $(LINUX_ARM64_FLAGS) -c -o $@ $< + +# Define object files for each platform and architecture +MACOS_X86_64_OBJ_FILES := $(patsubst %.c,$(BUILD_DIR)/macos/x86_64/%.o,$(SRC_FILES)) +MACOS_ARM64_OBJ_FILES := $(patsubst %.c,$(BUILD_DIR)/macos/arm64/%.o,$(SRC_FILES)) +LINUX_X86_64_OBJ_FILES := $(patsubst %.c,$(BUILD_DIR)/linux/x86_64/%.o,$(SRC_FILES)) +LINUX_ARM64_OBJ_FILES := $(patsubst %.c,$(BUILD_DIR)/linux/arm64/%.o,$(SRC_FILES)) + +# Create individual architecture libraries +$(DIST_DIR)/macos/libSimple_x86_64.a: $(MACOS_X86_64_OBJ_FILES) | $(DIST_DIR) + llvm-ar rc $@ $^ + +$(DIST_DIR)/macos/libSimple_arm64.a: $(MACOS_ARM64_OBJ_FILES) | $(DIST_DIR) + llvm-ar rc $@ $^ + +$(DIST_DIR)/linux/libSimple_x86_64.a: $(LINUX_X86_64_OBJ_FILES) | $(DIST_DIR) + llvm-ar rc $@ $^ + +$(DIST_DIR)/linux/libSimple_arm64.a: $(LINUX_ARM64_OBJ_FILES) | $(DIST_DIR) + llvm-ar rc $@ $^ + +# Create universal binary for macOS +$(DIST_DIR)/macos/libSimple.a: $(DIST_DIR)/macos/libSimple_x86_64.a $(DIST_DIR)/macos/libSimple_arm64.a + lipo -create -output $@ $^ + +# For Linux, we'll provide separate libraries since lipo is macOS-specific +linux: $(DIST_DIR)/linux/libSimple_x86_64.a $(DIST_DIR)/linux/libSimple_arm64.a + @echo "Linux libraries built in $(DIST_DIR)/linux/" + @echo "Note: For Linux, use the architecture-specific libraries as needed." + +macos: $(DIST_DIR)/macos/libSimple.a + @echo "macOS universal library built at $(DIST_DIR)/macos/libSimple.a" + +clean: + rm -rf $(BUILD_DIR) $(DIST_DIR) diff --git a/Fixtures/BinaryLibraries/Static/Package1/Simple.artifactbundle/dist/linux/libSimple_arm64.a b/Fixtures/BinaryLibraries/Static/Package1/Simple.artifactbundle/dist/linux/libSimple_arm64.a new file mode 100644 index 00000000000..1d7d4eb3cd0 Binary files /dev/null and b/Fixtures/BinaryLibraries/Static/Package1/Simple.artifactbundle/dist/linux/libSimple_arm64.a differ diff --git a/Fixtures/BinaryLibraries/Static/Package1/Simple.artifactbundle/dist/linux/libSimple_x86_64.a b/Fixtures/BinaryLibraries/Static/Package1/Simple.artifactbundle/dist/linux/libSimple_x86_64.a new file mode 100644 index 00000000000..95b8934db39 Binary files /dev/null and b/Fixtures/BinaryLibraries/Static/Package1/Simple.artifactbundle/dist/linux/libSimple_x86_64.a differ diff --git a/Fixtures/BinaryLibraries/Static/Package1/Simple.artifactbundle/dist/macos/libSimple.a b/Fixtures/BinaryLibraries/Static/Package1/Simple.artifactbundle/dist/macos/libSimple.a new file mode 100644 index 00000000000..9eba44b1920 Binary files /dev/null and b/Fixtures/BinaryLibraries/Static/Package1/Simple.artifactbundle/dist/macos/libSimple.a differ diff --git a/Fixtures/BinaryLibraries/Static/Package1/Simple.artifactbundle/dist/macos/libSimple_arm64.a b/Fixtures/BinaryLibraries/Static/Package1/Simple.artifactbundle/dist/macos/libSimple_arm64.a new file mode 100644 index 00000000000..e9ef8d3286f Binary files /dev/null and b/Fixtures/BinaryLibraries/Static/Package1/Simple.artifactbundle/dist/macos/libSimple_arm64.a differ diff --git a/Fixtures/BinaryLibraries/Static/Package1/Simple.artifactbundle/dist/macos/libSimple_x86_64.a b/Fixtures/BinaryLibraries/Static/Package1/Simple.artifactbundle/dist/macos/libSimple_x86_64.a new file mode 100644 index 00000000000..662beb03dc7 Binary files /dev/null and b/Fixtures/BinaryLibraries/Static/Package1/Simple.artifactbundle/dist/macos/libSimple_x86_64.a differ diff --git a/Fixtures/BinaryLibraries/Static/Package1/Simple.artifactbundle/include/simple.h b/Fixtures/BinaryLibraries/Static/Package1/Simple.artifactbundle/include/simple.h new file mode 100644 index 00000000000..5a2ca62ad29 --- /dev/null +++ b/Fixtures/BinaryLibraries/Static/Package1/Simple.artifactbundle/include/simple.h @@ -0,0 +1 @@ +int foo(void); diff --git a/Fixtures/BinaryLibraries/Static/Package1/Simple.artifactbundle/include/simple.modulemap b/Fixtures/BinaryLibraries/Static/Package1/Simple.artifactbundle/include/simple.modulemap new file mode 100644 index 00000000000..791601254a7 --- /dev/null +++ b/Fixtures/BinaryLibraries/Static/Package1/Simple.artifactbundle/include/simple.modulemap @@ -0,0 +1,4 @@ +module Simple { + header "simple.h" + export * +} \ No newline at end of file diff --git a/Fixtures/BinaryLibraries/Static/Package1/Simple.artifactbundle/info.json b/Fixtures/BinaryLibraries/Static/Package1/Simple.artifactbundle/info.json new file mode 100644 index 00000000000..96fed2184ac --- /dev/null +++ b/Fixtures/BinaryLibraries/Static/Package1/Simple.artifactbundle/info.json @@ -0,0 +1,35 @@ +{ + "schemaVersion": "1.0", + "artifacts": { + "simple": { + "type": "staticLibrary", + "version": "1.0.0", + "variants": [ + { + "path": "dist/macOS/libSimple.a", + "supportedTriples": ["arm64-apple-macosx", "x86_64-apple-macosx"], + "staticLibraryMetadata": { + "headerPaths": ["include"], + "moduleMapPath": "include/simple.modulemap" + } + }, + { + "path": "dist/linux/libSimple_arm64.a", + "supportedTriples": ["aarch64-unknown-linux-gnu"], + "staticLibraryMetadata": { + "headerPaths": ["include"], + "moduleMapPath": "include/simple.modulemap" + } + }, + { + "path": "dist/linux/libSimple_x86_64.a", + "supportedTriples": ["x86_64-unknown-linux-gnu"], + "staticLibraryMetadata": { + "headerPaths": ["include"], + "moduleMapPath": "include/simple.modulemap" + } + } + ] + } + } +} diff --git a/Fixtures/BinaryLibraries/Static/Package1/Simple.artifactbundle/simple.c b/Fixtures/BinaryLibraries/Static/Package1/Simple.artifactbundle/simple.c new file mode 100644 index 00000000000..464a2305647 --- /dev/null +++ b/Fixtures/BinaryLibraries/Static/Package1/Simple.artifactbundle/simple.c @@ -0,0 +1 @@ +int foo(void) { return 42; } diff --git a/Fixtures/BinaryLibraries/Static/Package1/Sources/Example/Example.swift b/Fixtures/BinaryLibraries/Static/Package1/Sources/Example/Example.swift new file mode 100644 index 00000000000..c17e5fce0df --- /dev/null +++ b/Fixtures/BinaryLibraries/Static/Package1/Sources/Example/Example.swift @@ -0,0 +1,10 @@ +import Simple +import Wrapper + +@main +struct Example { + static func main() { + print(foo()) + print(bar()) + } +} \ No newline at end of file diff --git a/Fixtures/BinaryLibraries/Static/Package1/Sources/Wrapper/include/wrapper.h b/Fixtures/BinaryLibraries/Static/Package1/Sources/Wrapper/include/wrapper.h new file mode 100644 index 00000000000..a0e75ca6d3b --- /dev/null +++ b/Fixtures/BinaryLibraries/Static/Package1/Sources/Wrapper/include/wrapper.h @@ -0,0 +1 @@ +int bar(void); diff --git a/Fixtures/BinaryLibraries/Static/Package1/Sources/Wrapper/wrapper.c b/Fixtures/BinaryLibraries/Static/Package1/Sources/Wrapper/wrapper.c new file mode 100644 index 00000000000..752298ee118 --- /dev/null +++ b/Fixtures/BinaryLibraries/Static/Package1/Sources/Wrapper/wrapper.c @@ -0,0 +1,3 @@ +#include "simple.h" + +int bar(void) { return foo(); } diff --git a/IntegrationTests/Fixtures/BinaryTargets/Inputs/DynamicLibrary/DynamicLibrary.m b/Fixtures/BinaryTargets/Inputs/DynamicLibrary/DynamicLibrary.m similarity index 100% rename from IntegrationTests/Fixtures/BinaryTargets/Inputs/DynamicLibrary/DynamicLibrary.m rename to Fixtures/BinaryTargets/Inputs/DynamicLibrary/DynamicLibrary.m diff --git a/IntegrationTests/Fixtures/BinaryTargets/Inputs/DynamicLibrary/include/DynamicLibrary.h b/Fixtures/BinaryTargets/Inputs/DynamicLibrary/include/DynamicLibrary.h similarity index 100% rename from IntegrationTests/Fixtures/BinaryTargets/Inputs/DynamicLibrary/include/DynamicLibrary.h rename to Fixtures/BinaryTargets/Inputs/DynamicLibrary/include/DynamicLibrary.h diff --git a/IntegrationTests/Fixtures/BinaryTargets/Inputs/StaticLibrary/StaticLibrary.m b/Fixtures/BinaryTargets/Inputs/StaticLibrary/StaticLibrary.m similarity index 100% rename from IntegrationTests/Fixtures/BinaryTargets/Inputs/StaticLibrary/StaticLibrary.m rename to Fixtures/BinaryTargets/Inputs/StaticLibrary/StaticLibrary.m diff --git a/IntegrationTests/Fixtures/BinaryTargets/Inputs/StaticLibrary/include/StaticLibrary.h b/Fixtures/BinaryTargets/Inputs/StaticLibrary/include/StaticLibrary.h similarity index 100% rename from IntegrationTests/Fixtures/BinaryTargets/Inputs/StaticLibrary/include/StaticLibrary.h rename to Fixtures/BinaryTargets/Inputs/StaticLibrary/include/StaticLibrary.h diff --git a/IntegrationTests/Fixtures/BinaryTargets/Inputs/SwiftFramework/SwiftFramework.xcodeproj/project.pbxproj b/Fixtures/BinaryTargets/Inputs/SwiftFramework/SwiftFramework.xcodeproj/project.pbxproj similarity index 100% rename from IntegrationTests/Fixtures/BinaryTargets/Inputs/SwiftFramework/SwiftFramework.xcodeproj/project.pbxproj rename to Fixtures/BinaryTargets/Inputs/SwiftFramework/SwiftFramework.xcodeproj/project.pbxproj diff --git a/IntegrationTests/Fixtures/BinaryTargets/Inputs/SwiftFramework/SwiftFramework.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Fixtures/BinaryTargets/Inputs/SwiftFramework/SwiftFramework.xcodeproj/project.xcworkspace/contents.xcworkspacedata similarity index 100% rename from IntegrationTests/Fixtures/BinaryTargets/Inputs/SwiftFramework/SwiftFramework.xcodeproj/project.xcworkspace/contents.xcworkspacedata rename to Fixtures/BinaryTargets/Inputs/SwiftFramework/SwiftFramework.xcodeproj/project.xcworkspace/contents.xcworkspacedata diff --git a/IntegrationTests/Fixtures/BinaryTargets/Inputs/SwiftFramework/SwiftFramework.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Fixtures/BinaryTargets/Inputs/SwiftFramework/SwiftFramework.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist similarity index 100% rename from IntegrationTests/Fixtures/BinaryTargets/Inputs/SwiftFramework/SwiftFramework.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist rename to Fixtures/BinaryTargets/Inputs/SwiftFramework/SwiftFramework.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist diff --git a/Fixtures/BinaryTargets/Inputs/SwiftFramework/SwiftFramework.xcodeproj/xcshareddata/xcschemes/SwiftFramework.xcscheme b/Fixtures/BinaryTargets/Inputs/SwiftFramework/SwiftFramework.xcodeproj/xcshareddata/xcschemes/SwiftFramework.xcscheme new file mode 100644 index 00000000000..3c42c6b98cd --- /dev/null +++ b/Fixtures/BinaryTargets/Inputs/SwiftFramework/SwiftFramework.xcodeproj/xcshareddata/xcschemes/SwiftFramework.xcscheme @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/IntegrationTests/Fixtures/BinaryTargets/Inputs/SwiftFramework/SwiftFramework/Info.plist b/Fixtures/BinaryTargets/Inputs/SwiftFramework/SwiftFramework/Info.plist similarity index 100% rename from IntegrationTests/Fixtures/BinaryTargets/Inputs/SwiftFramework/SwiftFramework/Info.plist rename to Fixtures/BinaryTargets/Inputs/SwiftFramework/SwiftFramework/Info.plist diff --git a/IntegrationTests/Fixtures/BinaryTargets/Inputs/SwiftFramework/SwiftFramework/SwiftFramework.h b/Fixtures/BinaryTargets/Inputs/SwiftFramework/SwiftFramework/SwiftFramework.h similarity index 100% rename from IntegrationTests/Fixtures/BinaryTargets/Inputs/SwiftFramework/SwiftFramework/SwiftFramework.h rename to Fixtures/BinaryTargets/Inputs/SwiftFramework/SwiftFramework/SwiftFramework.h diff --git a/IntegrationTests/Fixtures/BinaryTargets/Inputs/SwiftFramework/SwiftFramework/SwiftFramework.swift b/Fixtures/BinaryTargets/Inputs/SwiftFramework/SwiftFramework/SwiftFramework.swift similarity index 100% rename from IntegrationTests/Fixtures/BinaryTargets/Inputs/SwiftFramework/SwiftFramework/SwiftFramework.swift rename to Fixtures/BinaryTargets/Inputs/SwiftFramework/SwiftFramework/SwiftFramework.swift diff --git a/IntegrationTests/Fixtures/BinaryTargets/TestBinary/Package.swift b/Fixtures/BinaryTargets/TestBinary/Package.swift similarity index 100% rename from IntegrationTests/Fixtures/BinaryTargets/TestBinary/Package.swift rename to Fixtures/BinaryTargets/TestBinary/Package.swift diff --git a/IntegrationTests/Fixtures/BinaryTargets/TestBinary/Sources/CLibrary/CLibrary.m b/Fixtures/BinaryTargets/TestBinary/Sources/CLibrary/CLibrary.m similarity index 100% rename from IntegrationTests/Fixtures/BinaryTargets/TestBinary/Sources/CLibrary/CLibrary.m rename to Fixtures/BinaryTargets/TestBinary/Sources/CLibrary/CLibrary.m diff --git a/IntegrationTests/Fixtures/BinaryTargets/TestBinary/Sources/CLibrary/include/CLibrary.h b/Fixtures/BinaryTargets/TestBinary/Sources/CLibrary/include/CLibrary.h similarity index 100% rename from IntegrationTests/Fixtures/BinaryTargets/TestBinary/Sources/CLibrary/include/CLibrary.h rename to Fixtures/BinaryTargets/TestBinary/Sources/CLibrary/include/CLibrary.h diff --git a/IntegrationTests/Fixtures/BinaryTargets/TestBinary/Sources/Library/Library.swift b/Fixtures/BinaryTargets/TestBinary/Sources/Library/Library.swift similarity index 100% rename from IntegrationTests/Fixtures/BinaryTargets/TestBinary/Sources/Library/Library.swift rename to Fixtures/BinaryTargets/TestBinary/Sources/Library/Library.swift diff --git a/IntegrationTests/Fixtures/BinaryTargets/TestBinary/Sources/cexe/main.m b/Fixtures/BinaryTargets/TestBinary/Sources/cexe/main.m similarity index 100% rename from IntegrationTests/Fixtures/BinaryTargets/TestBinary/Sources/cexe/main.m rename to Fixtures/BinaryTargets/TestBinary/Sources/cexe/main.m diff --git a/IntegrationTests/Fixtures/BinaryTargets/TestBinary/Sources/exe/main.swift b/Fixtures/BinaryTargets/TestBinary/Sources/exe/main.swift similarity index 100% rename from IntegrationTests/Fixtures/BinaryTargets/TestBinary/Sources/exe/main.swift rename to Fixtures/BinaryTargets/TestBinary/Sources/exe/main.swift diff --git a/IntegrationTests/Fixtures/BinaryTargets/TestBinary/SwiftFramework.zip b/Fixtures/BinaryTargets/TestBinary/SwiftFramework.zip similarity index 100% rename from IntegrationTests/Fixtures/BinaryTargets/TestBinary/SwiftFramework.zip rename to Fixtures/BinaryTargets/TestBinary/SwiftFramework.zip diff --git a/Fixtures/CFamilyTargets/ModuleMapGenerationCases/Package.swift b/Fixtures/CFamilyTargets/ModuleMapGenerationCases/Package.swift index 04523ae3a07..bc7e8c77aa4 100644 --- a/Fixtures/CFamilyTargets/ModuleMapGenerationCases/Package.swift +++ b/Fixtures/CFamilyTargets/ModuleMapGenerationCases/Package.swift @@ -6,7 +6,10 @@ let package = Package( targets: [ .target( name: "Baz", - dependencies: ["FlatInclude", "NonModuleDirectoryInclude", "UmbrellaHeader", "UmbrellaDirectoryInclude", "UmbrellaHeaderFlat"]), + dependencies: ["CustomModuleMap", "FlatInclude", "NonModuleDirectoryInclude", "UmbrellaHeader", "UmbrellaDirectoryInclude", "UmbrellaHeaderFlat"]), + .target( + name: "CustomModuleMap", + dependencies: []), .target( name: "FlatInclude", dependencies: []), diff --git a/Fixtures/CFamilyTargets/ModuleMapGenerationCases/Sources/Baz/main.swift b/Fixtures/CFamilyTargets/ModuleMapGenerationCases/Sources/Baz/main.swift index 13f5c6f9fc8..f7a1f7f6500 100644 --- a/Fixtures/CFamilyTargets/ModuleMapGenerationCases/Sources/Baz/main.swift +++ b/Fixtures/CFamilyTargets/ModuleMapGenerationCases/Sources/Baz/main.swift @@ -1,3 +1,4 @@ +import CustomModuleMap import UmbrellaDirectoryInclude import FlatInclude import UmbrellaHeader @@ -7,3 +8,4 @@ let _ = foo() let _ = bar() let _ = jaz() let _ = umbrellaHeaderFlat() +let _ = qux() diff --git a/Fixtures/CFamilyTargets/ModuleMapGenerationCases/Sources/CustomModuleMap/CustomModuleMap.c b/Fixtures/CFamilyTargets/ModuleMapGenerationCases/Sources/CustomModuleMap/CustomModuleMap.c new file mode 100644 index 00000000000..472781ba8b7 --- /dev/null +++ b/Fixtures/CFamilyTargets/ModuleMapGenerationCases/Sources/CustomModuleMap/CustomModuleMap.c @@ -0,0 +1,8 @@ +#include "CustomModuleMap.h" + +int qux() { + int a = 6; + int b = a; + a = b; + return a; +} diff --git a/Fixtures/CFamilyTargets/ModuleMapGenerationCases/Sources/CustomModuleMap/include/CustomModuleMap.h b/Fixtures/CFamilyTargets/ModuleMapGenerationCases/Sources/CustomModuleMap/include/CustomModuleMap.h new file mode 100644 index 00000000000..9e9c8c63533 --- /dev/null +++ b/Fixtures/CFamilyTargets/ModuleMapGenerationCases/Sources/CustomModuleMap/include/CustomModuleMap.h @@ -0,0 +1 @@ +int qux(); diff --git a/Fixtures/CFamilyTargets/ModuleMapGenerationCases/Sources/CustomModuleMap/include/module.modulemap b/Fixtures/CFamilyTargets/ModuleMapGenerationCases/Sources/CustomModuleMap/include/module.modulemap new file mode 100644 index 00000000000..b4890569949 --- /dev/null +++ b/Fixtures/CFamilyTargets/ModuleMapGenerationCases/Sources/CustomModuleMap/include/module.modulemap @@ -0,0 +1,5 @@ +module CustomModuleMap { + header "CustomModuleMap.h" + + export * +} diff --git a/Fixtures/Coverage/Simple/Package.swift b/Fixtures/Coverage/Simple/Package.swift new file mode 100644 index 00000000000..4449f7edfff --- /dev/null +++ b/Fixtures/Coverage/Simple/Package.swift @@ -0,0 +1,26 @@ +// swift-tools-version: 6.2 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "Simple", + products: [ + // Products define the executables and libraries a package produces, making them visible to other packages. + .library( + name: "Simple", + targets: ["Simple"] + ), + ], + targets: [ + // Targets are the basic building blocks of a package, defining a module or a test suite. + // Targets can depend on other targets in this package and products from dependencies. + .target( + name: "Simple" + ), + .testTarget( + name: "SimpleTests", + dependencies: ["Simple"] + ), + ] +) diff --git a/Fixtures/Coverage/Simple/Sources/Simple/Simple.swift b/Fixtures/Coverage/Simple/Sources/Simple/Simple.swift new file mode 100644 index 00000000000..5e1e981ad5d --- /dev/null +++ b/Fixtures/Coverage/Simple/Sources/Simple/Simple.swift @@ -0,0 +1,10 @@ +// The Swift Programming Language +// https://docs.swift.org/swift-book + +public func greet(name: String = "world") -> String { + return "Hello, \(name)!" +} + +public func libA() -> String { + return "libA" +} diff --git a/Fixtures/Coverage/Simple/Tests/SimpleTests/SimpleTests.swift b/Fixtures/Coverage/Simple/Tests/SimpleTests/SimpleTests.swift new file mode 100644 index 00000000000..89e144011f1 --- /dev/null +++ b/Fixtures/Coverage/Simple/Tests/SimpleTests/SimpleTests.swift @@ -0,0 +1,24 @@ +import Testing +import XCTest +@testable import Simple + +@Test( + arguments: [ + "Bob", + "Alice", + "", + ] +) + func testGreet( + name: String + ) async throws { + let actual = greet(name: name) + + #expect(actual == "Hello, \(name)!") +} + +final class SimpleTests: XCTestCase { + func testExample() throws { + XCTAssertEqual(libA(), "libA", "Actual is not as expected") + } +} diff --git a/Fixtures/DependencyResolution/External/Complex/FisherYates/src/Fisher-Yates_Shuffle.swift b/Fixtures/DependencyResolution/External/Complex/FisherYates/src/Fisher-Yates_Shuffle.swift index 9c066414e6b..0d773ea77da 100644 --- a/Fixtures/DependencyResolution/External/Complex/FisherYates/src/Fisher-Yates_Shuffle.swift +++ b/Fixtures/DependencyResolution/External/Complex/FisherYates/src/Fisher-Yates_Shuffle.swift @@ -1,10 +1,3 @@ -#if os(macOS) || os(iOS) -import Darwin -#elseif canImport(Glibc) -import Glibc -#elseif canImport(Musl) -import Musl -#endif public extension Collection { func shuffle() -> [Iterator.Element] { @@ -22,15 +15,13 @@ public extension MutableCollection { guard c > 1 else { return } for (firstUnshuffled, unshuffledCount) in zip(indices, stride(from: c, to: 1, by: -1)) { -#if os(macOS) || os(iOS) - let d = arc4random_uniform(numericCast(unshuffledCount)) -#else - let d = numericCast(random()) % unshuffledCount -#endif - let i = index(firstUnshuffled, offsetBy: numericCast(d)) + var g = SystemRandomNumberGenerator() + let d = Int.random(in: 1...unshuffledCount, using: &g) + let i = index(firstUnshuffled, offsetBy: d) swapAt(firstUnshuffled, i) } } } + public let shuffle = false diff --git a/Fixtures/DependencyResolution/External/Complex/deck-of-playing-cards-local/Package.swift b/Fixtures/DependencyResolution/External/Complex/deck-of-playing-cards-local/Package.swift new file mode 100644 index 00000000000..94e46de118a --- /dev/null +++ b/Fixtures/DependencyResolution/External/Complex/deck-of-playing-cards-local/Package.swift @@ -0,0 +1,19 @@ +// swift-tools-version:4.2 +import PackageDescription + +let package = Package( + name: "DeckOfPlayingCards", + products: [ + .library(name: "DeckOfPlayingCards", targets: ["DeckOfPlayingCards"]), + ], + dependencies: [ + .package(path: "../PlayingCard"), + .package(path: "../FisherYates") + ], + targets: [ + .target( + name: "DeckOfPlayingCards", + dependencies: ["PlayingCard", "FisherYates"], + path: "src"), + ] +) diff --git a/Fixtures/DependencyResolution/External/Complex/deck-of-playing-cards-local/src/Deck.swift b/Fixtures/DependencyResolution/External/Complex/deck-of-playing-cards-local/src/Deck.swift new file mode 100644 index 00000000000..e69de29bb2d diff --git a/Fixtures/Miscellaneous/APIDiff/CIncludePath/Package.swift b/Fixtures/Miscellaneous/APIDiff/CIncludePath/Package.swift new file mode 100644 index 00000000000..957899aa732 --- /dev/null +++ b/Fixtures/Miscellaneous/APIDiff/CIncludePath/Package.swift @@ -0,0 +1,27 @@ +// swift-tools-version: 6.0 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "Sample", + products: [ + .library( + name: "Sample", + targets: ["Sample"] + ), + ], + targets: [ + .target( + name: "CSample", + sources: ["./vendorsrc/src"], + cSettings: [ + .headerSearchPath("./vendorsrc/include"), + ] + ), + .target( + name: "Sample", + dependencies: ["CSample"] + ), + ] +) diff --git a/Fixtures/Miscellaneous/APIDiff/CIncludePath/Sources/CSample/include/CSample.h b/Fixtures/Miscellaneous/APIDiff/CIncludePath/Sources/CSample/include/CSample.h new file mode 100644 index 00000000000..f23f5ff29d4 --- /dev/null +++ b/Fixtures/Miscellaneous/APIDiff/CIncludePath/Sources/CSample/include/CSample.h @@ -0,0 +1,3 @@ + +#include "config.h" +#include "../vendorsrc/include/vendor.h" \ No newline at end of file diff --git a/Fixtures/Miscellaneous/APIDiff/CIncludePath/Sources/CSample/include/config.h b/Fixtures/Miscellaneous/APIDiff/CIncludePath/Sources/CSample/include/config.h new file mode 100644 index 00000000000..4af5cd32ee8 --- /dev/null +++ b/Fixtures/Miscellaneous/APIDiff/CIncludePath/Sources/CSample/include/config.h @@ -0,0 +1 @@ +#define HAVE_VENDOR_CONFIG diff --git a/Fixtures/Miscellaneous/APIDiff/CIncludePath/Sources/CSample/vendorsrc/include/vendor.h b/Fixtures/Miscellaneous/APIDiff/CIncludePath/Sources/CSample/vendorsrc/include/vendor.h new file mode 100644 index 00000000000..6ac629b8596 --- /dev/null +++ b/Fixtures/Miscellaneous/APIDiff/CIncludePath/Sources/CSample/vendorsrc/include/vendor.h @@ -0,0 +1,2 @@ + +#include "config.h" diff --git a/Fixtures/Miscellaneous/APIDiff/CIncludePath/Sources/CSample/vendorsrc/src/vendor.c b/Fixtures/Miscellaneous/APIDiff/CIncludePath/Sources/CSample/vendorsrc/src/vendor.c new file mode 100644 index 00000000000..d36cd772385 --- /dev/null +++ b/Fixtures/Miscellaneous/APIDiff/CIncludePath/Sources/CSample/vendorsrc/src/vendor.c @@ -0,0 +1,6 @@ +#include "vendor.h" + +int vendor__func(int n) +{ + return 0; +} diff --git a/Fixtures/Miscellaneous/APIDiff/CIncludePath/Sources/Sample/Sample.swift b/Fixtures/Miscellaneous/APIDiff/CIncludePath/Sources/Sample/Sample.swift new file mode 100644 index 00000000000..c171c510e13 --- /dev/null +++ b/Fixtures/Miscellaneous/APIDiff/CIncludePath/Sources/Sample/Sample.swift @@ -0,0 +1 @@ +import CSample diff --git a/Fixtures/Miscellaneous/APIDiff/WithPlugin/Package.swift b/Fixtures/Miscellaneous/APIDiff/WithPlugin/Package.swift new file mode 100644 index 00000000000..8e0ea4228e5 --- /dev/null +++ b/Fixtures/Miscellaneous/APIDiff/WithPlugin/Package.swift @@ -0,0 +1,16 @@ +// swift-tools-version: 6.1 +import PackageDescription + +let package = Package( + name: "package-with-plugin", + products: [.library(name: "PackageLib", targets: ["TargetLib"])], + targets: [ + .target(name: "TargetLib"), + .executableTarget(name: "BuildTool", dependencies: ["TargetLib"]), + .plugin( + name: "BuildPlugin", + capability: .command(intent: .custom(verb: "do-it-now", description: "")), + dependencies: ["BuildTool"] + ), + ] +) diff --git a/Fixtures/Miscellaneous/APIDiff/WithPlugin/Plugins/BuildPlugin/BuildToolPlugin.swift b/Fixtures/Miscellaneous/APIDiff/WithPlugin/Plugins/BuildPlugin/BuildToolPlugin.swift new file mode 100644 index 00000000000..d83cdcf2c4f --- /dev/null +++ b/Fixtures/Miscellaneous/APIDiff/WithPlugin/Plugins/BuildPlugin/BuildToolPlugin.swift @@ -0,0 +1,9 @@ +import Foundation +import PackagePlugin + +@main +final class BuildToolPlugin: CommandPlugin { + func performCommand(context: PluginContext, arguments: [String]) async throws { + _ = try context.tool(named: "BuildTool") + } +} diff --git a/Fixtures/Miscellaneous/APIDiff/WithPlugin/Sources/BuildTool/BuildTool.swift b/Fixtures/Miscellaneous/APIDiff/WithPlugin/Sources/BuildTool/BuildTool.swift new file mode 100644 index 00000000000..02da39c58bf --- /dev/null +++ b/Fixtures/Miscellaneous/APIDiff/WithPlugin/Sources/BuildTool/BuildTool.swift @@ -0,0 +1,8 @@ +import TargetLib + +@main +public struct BuildTool { + static func main() { + TargetLibStruct.do_it() + } +} diff --git a/Fixtures/Miscellaneous/APIDiff/WithPlugin/Sources/TargetLib/TargetLib.swift b/Fixtures/Miscellaneous/APIDiff/WithPlugin/Sources/TargetLib/TargetLib.swift new file mode 100644 index 00000000000..c351ce56687 --- /dev/null +++ b/Fixtures/Miscellaneous/APIDiff/WithPlugin/Sources/TargetLib/TargetLib.swift @@ -0,0 +1,5 @@ +public enum TargetLibStruct { + public static func do_it() { + print("Hello!") + } +} diff --git a/Fixtures/Miscellaneous/AtMainSupport/Package.swift b/Fixtures/Miscellaneous/AtMainSupport/Package.swift index 525f226a1e0..59d76fa7e90 100644 --- a/Fixtures/Miscellaneous/AtMainSupport/Package.swift +++ b/Fixtures/Miscellaneous/AtMainSupport/Package.swift @@ -9,7 +9,10 @@ let package = Package( .executable(name: "SwiftExecMultiFile", targets: ["SwiftExecMultiFile"]), ], targets: [ - .executableTarget(name: "ClangExecSingleFile"), + .executableTarget(name: "ClangExecSingleFile", + linkerSettings: [ + .linkedLibrary("swiftCore", .when(platforms: [.windows])), // for swift_addNewDSOImage + ]), .executableTarget(name: "SwiftExecSingleFile"), .executableTarget(name: "SwiftExecMultiFile"), ] diff --git a/Fixtures/Miscellaneous/CXX17CompilerCrash/v5_7/include/user_objects.h b/Fixtures/Miscellaneous/CXX17CompilerCrash/v5_7/include/user_objects.h index d4ba50b3c08..471c383a22f 100644 --- a/Fixtures/Miscellaneous/CXX17CompilerCrash/v5_7/include/user_objects.h +++ b/Fixtures/Miscellaneous/CXX17CompilerCrash/v5_7/include/user_objects.h @@ -89,7 +89,7 @@ class mjCError { // alternative specifications of frame orientation class mjCAlternative { public: - mjCAlternative(); // constuctor + mjCAlternative(); // constructor const char* Set(double* quat, double* inertia, // set frame quat and diag. inertia bool degree, // angle format: degree/radian const char* sequence); // euler sequence format: "xyz" diff --git a/Fixtures/Miscellaneous/CXX17CompilerCrash/v5_7/lodepng/include/lodepng.h b/Fixtures/Miscellaneous/CXX17CompilerCrash/v5_7/lodepng/include/lodepng.h index 685e7850cbd..23791aba1a4 100644 --- a/Fixtures/Miscellaneous/CXX17CompilerCrash/v5_7/lodepng/include/lodepng.h +++ b/Fixtures/Miscellaneous/CXX17CompilerCrash/v5_7/lodepng/include/lodepng.h @@ -930,7 +930,7 @@ unsigned lodepng_chunk_append(unsigned char** out, size_t* outsize, const unsign Appends new chunk to out. The chunk to append is given by giving its length, type and data separately. The type is a 4-letter string. The out variable and outsize are updated to reflect the new reallocated buffer. -Returne error code (0 if it went ok) +Returns error code (0 if it went ok) */ unsigned lodepng_chunk_create(unsigned char** out, size_t* outsize, unsigned length, const char* type, const unsigned char* data); @@ -1807,7 +1807,7 @@ state.decoder.remember_unknown_chunks: whether to read in unknown chunks state.info_raw.colortype: desired color type for decoded image state.info_raw.bitdepth: desired bit depth for decoded image state.info_raw....: more color settings, see struct LodePNGColorMode -state.info_png....: no settings for decoder but ouput, see struct LodePNGInfo +state.info_png....: no settings for decoder but output, see struct LodePNGInfo For encoding: diff --git a/Fixtures/Miscellaneous/CXX17CompilerCrash/v5_8/include/user_objects.h b/Fixtures/Miscellaneous/CXX17CompilerCrash/v5_8/include/user_objects.h index d4ba50b3c08..471c383a22f 100644 --- a/Fixtures/Miscellaneous/CXX17CompilerCrash/v5_8/include/user_objects.h +++ b/Fixtures/Miscellaneous/CXX17CompilerCrash/v5_8/include/user_objects.h @@ -89,7 +89,7 @@ class mjCError { // alternative specifications of frame orientation class mjCAlternative { public: - mjCAlternative(); // constuctor + mjCAlternative(); // constructor const char* Set(double* quat, double* inertia, // set frame quat and diag. inertia bool degree, // angle format: degree/radian const char* sequence); // euler sequence format: "xyz" diff --git a/Fixtures/Miscellaneous/CXX17CompilerCrash/v5_8/lodepng/include/lodepng.h b/Fixtures/Miscellaneous/CXX17CompilerCrash/v5_8/lodepng/include/lodepng.h index 685e7850cbd..23791aba1a4 100644 --- a/Fixtures/Miscellaneous/CXX17CompilerCrash/v5_8/lodepng/include/lodepng.h +++ b/Fixtures/Miscellaneous/CXX17CompilerCrash/v5_8/lodepng/include/lodepng.h @@ -930,7 +930,7 @@ unsigned lodepng_chunk_append(unsigned char** out, size_t* outsize, const unsign Appends new chunk to out. The chunk to append is given by giving its length, type and data separately. The type is a 4-letter string. The out variable and outsize are updated to reflect the new reallocated buffer. -Returne error code (0 if it went ok) +Returns error code (0 if it went ok) */ unsigned lodepng_chunk_create(unsigned char** out, size_t* outsize, unsigned length, const char* type, const unsigned char* data); @@ -1807,7 +1807,7 @@ state.decoder.remember_unknown_chunks: whether to read in unknown chunks state.info_raw.colortype: desired color type for decoded image state.info_raw.bitdepth: desired bit depth for decoded image state.info_raw....: more color settings, see struct LodePNGColorMode -state.info_png....: no settings for decoder but ouput, see struct LodePNGInfo +state.info_png....: no settings for decoder but output, see struct LodePNGInfo For encoding: diff --git a/Fixtures/Miscellaneous/CheckTestLibraryEnvironmentVariable/Package.swift b/Fixtures/Miscellaneous/CheckTestLibraryEnvironmentVariable/Package.swift new file mode 100644 index 00000000000..11f57d12699 --- /dev/null +++ b/Fixtures/Miscellaneous/CheckTestLibraryEnvironmentVariable/Package.swift @@ -0,0 +1,10 @@ +// swift-tools-version: 5.10 + +import PackageDescription + +let package = Package( + name: "CheckTestLibraryEnvironmentVariable", + targets: [ + .testTarget(name: "CheckTestLibraryEnvironmentVariableTests"), + ] +) diff --git a/Fixtures/Miscellaneous/CheckTestLibraryEnvironmentVariable/Tests/CheckTestLibraryEnvironmentVariableTests/CheckTestLibraryEnvironmentVariableTests.swift b/Fixtures/Miscellaneous/CheckTestLibraryEnvironmentVariable/Tests/CheckTestLibraryEnvironmentVariableTests/CheckTestLibraryEnvironmentVariableTests.swift new file mode 100644 index 00000000000..66492767d31 --- /dev/null +++ b/Fixtures/Miscellaneous/CheckTestLibraryEnvironmentVariable/Tests/CheckTestLibraryEnvironmentVariableTests/CheckTestLibraryEnvironmentVariableTests.swift @@ -0,0 +1,21 @@ +import XCTest + +final class CheckTestLibraryEnvironmentVariableTests: XCTestCase { + func testEnvironmentVariables() throws { + #if !os(macOS) + try XCTSkipIf(true, "Test is macOS specific") + #endif + + let testingEnabled = ProcessInfo.processInfo.environment["SWIFT_TESTING_ENABLED"] + XCTAssertEqual(testingEnabled, "0") + + if ProcessInfo.processInfo.environment["CONTAINS_SWIFT_TESTING"] != nil { + let frameworkPath = try XCTUnwrap(ProcessInfo.processInfo.environment["DYLD_FRAMEWORK_PATH"]) + let libraryPath = try XCTUnwrap(ProcessInfo.processInfo.environment["DYLD_LIBRARY_PATH"]) + XCTAssertTrue( + frameworkPath.contains("testing") || libraryPath.contains("testing"), + "Expected 'testing' in '\(frameworkPath)' or '\(libraryPath)'" + ) + } + } +} diff --git a/Fixtures/Miscellaneous/DifferentProductTargetName/Package.swift b/Fixtures/Miscellaneous/DifferentProductTargetName/Package.swift new file mode 100644 index 00000000000..3e4054390c3 --- /dev/null +++ b/Fixtures/Miscellaneous/DifferentProductTargetName/Package.swift @@ -0,0 +1,12 @@ +// swift-tools-version:5.2 +import PackageDescription + +let package = Package( + name: "Foo", + products: [ + .executable(name: "Foo", targets: ["Bar"]), + ], + targets: [ + .target(name: "Bar", path: "./"), + ] +) diff --git a/Fixtures/Miscellaneous/DifferentProductTargetName/main.swift b/Fixtures/Miscellaneous/DifferentProductTargetName/main.swift new file mode 100644 index 00000000000..e69de29bb2d diff --git a/Fixtures/Miscellaneous/DoNotFilterLinkerDiagnostics/Package.swift b/Fixtures/Miscellaneous/DoNotFilterLinkerDiagnostics/Package.swift new file mode 100644 index 00000000000..636e93fcc67 --- /dev/null +++ b/Fixtures/Miscellaneous/DoNotFilterLinkerDiagnostics/Package.swift @@ -0,0 +1,15 @@ +// swift-tools-version: 6.0 + +import PackageDescription + +let package = Package( + name: "DoNotFilterLinkerDiagnostics", + targets: [ + .executableTarget( + name: "DoNotFilterLinkerDiagnostics", + linkerSettings: [ + .unsafeFlags(["-Lfoobar"]), + ] + ), + ] +) diff --git a/Fixtures/Miscellaneous/DoNotFilterLinkerDiagnostics/Sources/DoNotFilterLinkerDiagnostics/main.swift b/Fixtures/Miscellaneous/DoNotFilterLinkerDiagnostics/Sources/DoNotFilterLinkerDiagnostics/main.swift new file mode 100644 index 00000000000..44e20d5acc4 --- /dev/null +++ b/Fixtures/Miscellaneous/DoNotFilterLinkerDiagnostics/Sources/DoNotFilterLinkerDiagnostics/main.swift @@ -0,0 +1,4 @@ +// The Swift Programming Language +// https://docs.swift.org/swift-book + +print("Hello, world!") diff --git a/Fixtures/Miscellaneous/DumpPackage/PlayingCard/Package.swift b/Fixtures/Miscellaneous/DumpPackage/PlayingCard/Package.swift new file mode 100644 index 00000000000..ef346d15f66 --- /dev/null +++ b/Fixtures/Miscellaneous/DumpPackage/PlayingCard/Package.swift @@ -0,0 +1,12 @@ +// swift-tools-version: 6.0 +import PackageDescription + +let package = Package( + name: "PlayingCard", + products: [ + .library(name: "PlayingCard", targets: ["PlayingCard"]), + ], + targets: [ + .target(name: "PlayingCard", path: "src"), + ] +) diff --git a/Fixtures/Miscellaneous/DumpPackage/PlayingCard/src/PlayingCard.swift b/Fixtures/Miscellaneous/DumpPackage/PlayingCard/src/PlayingCard.swift new file mode 100644 index 00000000000..bbbb52deb7d --- /dev/null +++ b/Fixtures/Miscellaneous/DumpPackage/PlayingCard/src/PlayingCard.swift @@ -0,0 +1,25 @@ +public struct PlayingCard: Equatable { + let rank: Rank + let suit: Suit + + public init(rank: Rank, suit: Suit) { + self.rank = rank + self.suit = suit + } +} + +// MARK: - Comparable + +extension PlayingCard: Comparable {} + +public func <(lhs: PlayingCard, rhs: PlayingCard) -> Bool { + return lhs.suit < rhs.suit || (lhs.suit == rhs.suit && lhs.rank < rhs.rank) +} + +// MARK: - CustomStringConvertible + +extension PlayingCard : CustomStringConvertible { + public var description: String { + return "\(suit)\(rank)" + } +} diff --git a/Fixtures/Miscellaneous/DumpPackage/PlayingCard/src/Rank.swift b/Fixtures/Miscellaneous/DumpPackage/PlayingCard/src/Rank.swift new file mode 100644 index 00000000000..530371d5f39 --- /dev/null +++ b/Fixtures/Miscellaneous/DumpPackage/PlayingCard/src/Rank.swift @@ -0,0 +1,35 @@ +public enum Rank : Int { + case Ace = 1 + case Two, Three, Four, Five, Six, Seven, Eight, Nine, Ten + case Jack, Queen, King +} + +// MARK: - Comparable + +extension Rank : Comparable {} + +public func <(lhs: Rank, rhs: Rank) -> Bool { + switch (lhs, rhs) { + case (_, _) where lhs == rhs: + return false + case (.Ace, _): + return false + default: + return lhs.rawValue < rhs.rawValue + } +} + +// MARK: - CustomStringConvertible + +extension Rank : CustomStringConvertible { + public var description: String { + switch self { + case .Ace: return "A" + case .Jack: return "J" + case .Queen: return "Q" + case .King: return "K" + default: + return "\(rawValue)" + } + } +} diff --git a/Fixtures/Miscellaneous/DumpPackage/PlayingCard/src/Suit.swift b/Fixtures/Miscellaneous/DumpPackage/PlayingCard/src/Suit.swift new file mode 100644 index 00000000000..8fabbc4d86b --- /dev/null +++ b/Fixtures/Miscellaneous/DumpPackage/PlayingCard/src/Suit.swift @@ -0,0 +1,33 @@ +public enum Suit: String { + case Spades, Hearts, Diamonds, Clubs +} + +// MARK: - Comparable + +extension Suit: Comparable {} + +public func <(lhs: Suit, rhs: Suit) -> Bool { + switch (lhs, rhs) { + case (_, _) where lhs == rhs: + return false + case (.Spades, _), + (.Hearts, .Diamonds), (.Hearts, .Clubs), + (.Diamonds, .Clubs): + return false + default: + return true + } +} + +// MARK: - CustomStringConvertible + +extension Suit : CustomStringConvertible { + public var description: String { + switch self { + case .Spades: return "♠︎" + case .Hearts: return "♡" + case .Diamonds: return "♢" + case .Clubs: return "♣︎" + } + } +} diff --git a/Fixtures/Miscellaneous/DumpPackage/app/Package.swift b/Fixtures/Miscellaneous/DumpPackage/app/Package.swift new file mode 100644 index 00000000000..dcf621e8e8c --- /dev/null +++ b/Fixtures/Miscellaneous/DumpPackage/app/Package.swift @@ -0,0 +1,23 @@ +// swift-tools-version: 6.0 +import PackageDescription + +let package = Package( + name: "Dealer", + defaultLocalization: "en", + platforms: [ + .macOS(.v10_13), + .iOS(.v12), + .tvOS(.v12), + .watchOS(.v5) + ], + dependencies: [ + .package(path: "../PlayingCard"), + ], + targets: [ + .target( + name: "Dealer", + dependencies: ["PlayingCard"], + path: "./" + ), + ] +) diff --git a/Fixtures/Miscellaneous/DumpPackage/app/main.swift b/Fixtures/Miscellaneous/DumpPackage/app/main.swift new file mode 100644 index 00000000000..4cf8762b715 --- /dev/null +++ b/Fixtures/Miscellaneous/DumpPackage/app/main.swift @@ -0,0 +1,20 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2014 - 2017 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 Swift project authors +*/ + +import PlayingCard + +let hand: [PlayingCard] = [ + .init(rank: .Ace, suit: .Hearts), + .init(rank: .King, suit: .Spades), +] + +for card in hand { + print(card) +} diff --git a/Fixtures/Miscellaneous/DynamicProduct/exec/Package.swift b/Fixtures/Miscellaneous/DynamicProduct/exec/Package.swift new file mode 100644 index 00000000000..df567ec10c5 --- /dev/null +++ b/Fixtures/Miscellaneous/DynamicProduct/exec/Package.swift @@ -0,0 +1,18 @@ +// swift-tools-version:6.0 + +import PackageDescription + +let package = Package( + name: "exec", + dependencies: [ + .package(path: "../secondDyna") + ], + targets: [ + .executableTarget( + name: "exec", + dependencies: ["secondDyna"]), + .testTarget( + name: "DynaTests", + dependencies: ["exec"]) + ] +) diff --git a/Fixtures/Miscellaneous/DynamicProduct/exec/README.md b/Fixtures/Miscellaneous/DynamicProduct/exec/README.md new file mode 100644 index 00000000000..0d89299ffaa --- /dev/null +++ b/Fixtures/Miscellaneous/DynamicProduct/exec/README.md @@ -0,0 +1,3 @@ +# exec + +A description of this package. diff --git a/Fixtures/Miscellaneous/DynamicProduct/exec/Sources/exec/main.swift b/Fixtures/Miscellaneous/DynamicProduct/exec/Sources/exec/main.swift new file mode 100644 index 00000000000..b11e9813d45 --- /dev/null +++ b/Fixtures/Miscellaneous/DynamicProduct/exec/Sources/exec/main.swift @@ -0,0 +1,3 @@ +import secondDyna + +print(secondDyna.hello()) diff --git a/Fixtures/Miscellaneous/DynamicProduct/exec/Tests/DynaTests/Tests.swift b/Fixtures/Miscellaneous/DynamicProduct/exec/Tests/DynaTests/Tests.swift new file mode 100644 index 00000000000..8276753746b --- /dev/null +++ b/Fixtures/Miscellaneous/DynamicProduct/exec/Tests/DynaTests/Tests.swift @@ -0,0 +1 @@ +error diff --git a/Fixtures/Miscellaneous/DynamicProduct/firstDyna/Package.swift b/Fixtures/Miscellaneous/DynamicProduct/firstDyna/Package.swift new file mode 100644 index 00000000000..c87a41c7b15 --- /dev/null +++ b/Fixtures/Miscellaneous/DynamicProduct/firstDyna/Package.swift @@ -0,0 +1,21 @@ +// swift-tools-version:5.0 + +import PackageDescription + +let package = Package( + name: "firstDyna", + products: [ + .library( + name: "firstDyna", + type: .dynamic, + targets: ["firstDyna"]) + ], + targets: [ + .target( + name: "firstDyna", + dependencies: ["Core"]), + .target( + name: "Core", + dependencies: []) + ] +) diff --git a/Fixtures/Miscellaneous/DynamicProduct/firstDyna/README.md b/Fixtures/Miscellaneous/DynamicProduct/firstDyna/README.md new file mode 100644 index 00000000000..64bf4ae9fe5 --- /dev/null +++ b/Fixtures/Miscellaneous/DynamicProduct/firstDyna/README.md @@ -0,0 +1,3 @@ +# firstDyna + +A description of this package. diff --git a/Fixtures/Miscellaneous/DynamicProduct/firstDyna/Sources/Core/Core.c b/Fixtures/Miscellaneous/DynamicProduct/firstDyna/Sources/Core/Core.c new file mode 100644 index 00000000000..8ed9eb96306 --- /dev/null +++ b/Fixtures/Miscellaneous/DynamicProduct/firstDyna/Sources/Core/Core.c @@ -0,0 +1,5 @@ +#include + +char* hello() { + return "Hello"; +} diff --git a/Fixtures/Miscellaneous/DynamicProduct/firstDyna/Sources/Core/include/Core.h b/Fixtures/Miscellaneous/DynamicProduct/firstDyna/Sources/Core/include/Core.h new file mode 100644 index 00000000000..c8d3d83ef42 --- /dev/null +++ b/Fixtures/Miscellaneous/DynamicProduct/firstDyna/Sources/Core/include/Core.h @@ -0,0 +1,2 @@ + +char* hello(); diff --git a/Fixtures/Miscellaneous/DynamicProduct/firstDyna/Sources/firstDyna/firstDyna.swift b/Fixtures/Miscellaneous/DynamicProduct/firstDyna/Sources/firstDyna/firstDyna.swift new file mode 100644 index 00000000000..667df7570ee --- /dev/null +++ b/Fixtures/Miscellaneous/DynamicProduct/firstDyna/Sources/firstDyna/firstDyna.swift @@ -0,0 +1,5 @@ +import Core + +public func hello() -> String { + return String(cString: Core.hello()) +} diff --git a/Fixtures/Miscellaneous/DynamicProduct/secondDyna/Package.swift b/Fixtures/Miscellaneous/DynamicProduct/secondDyna/Package.swift new file mode 100644 index 00000000000..885eac88dd5 --- /dev/null +++ b/Fixtures/Miscellaneous/DynamicProduct/secondDyna/Package.swift @@ -0,0 +1,21 @@ +// swift-tools-version:5.0 + +import PackageDescription + +let package = Package( + name: "secondDyna", + products: [ + .library( + name: "secondDyna", + type: .dynamic, + targets: ["secondDyna"]) + ], + dependencies: [ + .package(path: "../firstDyna") + ], + targets: [ + .target( + name: "secondDyna", + dependencies: ["firstDyna"]) + ] +) diff --git a/Fixtures/Miscellaneous/DynamicProduct/secondDyna/README.md b/Fixtures/Miscellaneous/DynamicProduct/secondDyna/README.md new file mode 100644 index 00000000000..0d8dc15e355 --- /dev/null +++ b/Fixtures/Miscellaneous/DynamicProduct/secondDyna/README.md @@ -0,0 +1,3 @@ +# secondDyna + +A description of this package. diff --git a/Fixtures/Miscellaneous/DynamicProduct/secondDyna/Sources/secondDyna/secondDyna.swift b/Fixtures/Miscellaneous/DynamicProduct/secondDyna/Sources/secondDyna/secondDyna.swift new file mode 100644 index 00000000000..cde769ba09b --- /dev/null +++ b/Fixtures/Miscellaneous/DynamicProduct/secondDyna/Sources/secondDyna/secondDyna.swift @@ -0,0 +1,5 @@ +import firstDyna + +public func hello() -> String { + return firstDyna.hello() +} diff --git a/Fixtures/Miscellaneous/EchoExecutable/Package.swift b/Fixtures/Miscellaneous/EchoExecutable/Package.swift index ec5a31ab18c..13570183d6d 100644 --- a/Fixtures/Miscellaneous/EchoExecutable/Package.swift +++ b/Fixtures/Miscellaneous/EchoExecutable/Package.swift @@ -7,5 +7,6 @@ let package = Package( .executable(name: "secho", targets: ["secho"]) ], targets: [ - .target(name: "secho", dependencies: []) - ]) \ No newline at end of file + .target(name: "secho", dependencies: []), + .testTarget(name: "TestSuite") + ]) diff --git a/Fixtures/Miscellaneous/EchoExecutable/Sources/secho/main.swift b/Fixtures/Miscellaneous/EchoExecutable/Sources/secho/main.swift index 1b5b30cbbd5..3ee98ead3e3 100644 --- a/Fixtures/Miscellaneous/EchoExecutable/Sources/secho/main.swift +++ b/Fixtures/Miscellaneous/EchoExecutable/Sources/secho/main.swift @@ -2,8 +2,14 @@ import Glibc #elseif canImport(Musl) import Musl -#else +#elseif canImport(Android) + import Android +#elseif canImport(Darwin.C) import Darwin.C +#elseif canImport(ucrt) + import ucrt + let PATH_MAX = 260 + typealias Int = Int32 #endif let cwd = getcwd(nil, Int(PATH_MAX)) diff --git a/Fixtures/Miscellaneous/EchoExecutable/Tests/TestSuite/Tests.swift b/Fixtures/Miscellaneous/EchoExecutable/Tests/TestSuite/Tests.swift new file mode 100644 index 00000000000..8bf929fa594 --- /dev/null +++ b/Fixtures/Miscellaneous/EchoExecutable/Tests/TestSuite/Tests.swift @@ -0,0 +1,7 @@ +import XCTest + +final class TestCase: XCTestCase { + func testFoo() { + XCTAssertTrue(true) + } +} diff --git a/Fixtures/Miscellaneous/EchoExecutable/echo.bat b/Fixtures/Miscellaneous/EchoExecutable/echo.bat new file mode 100644 index 00000000000..52f6b0045f6 --- /dev/null +++ b/Fixtures/Miscellaneous/EchoExecutable/echo.bat @@ -0,0 +1,2 @@ +echo sentinel +echo %* diff --git a/Fixtures/Miscellaneous/EchoExecutable/echo.sh b/Fixtures/Miscellaneous/EchoExecutable/echo.sh new file mode 100755 index 00000000000..40f21597c66 --- /dev/null +++ b/Fixtures/Miscellaneous/EchoExecutable/echo.sh @@ -0,0 +1,4 @@ +#!/bin/sh + +echo sentinel +echo "$@" diff --git a/Fixtures/Miscellaneous/EchoExecutable/toolset.json b/Fixtures/Miscellaneous/EchoExecutable/toolset.json new file mode 100644 index 00000000000..1092b5740c3 --- /dev/null +++ b/Fixtures/Miscellaneous/EchoExecutable/toolset.json @@ -0,0 +1,6 @@ +{ + "debugger": { "path": "echo.sh" }, + "testRunner": { "path": "echo.sh" }, + "schemaVersion" : "1.0", + "rootPath" : "." +} diff --git a/Fixtures/Miscellaneous/EchoExecutable/toolset.win32.json b/Fixtures/Miscellaneous/EchoExecutable/toolset.win32.json new file mode 100644 index 00000000000..f220cc9a5fa --- /dev/null +++ b/Fixtures/Miscellaneous/EchoExecutable/toolset.win32.json @@ -0,0 +1,6 @@ +{ + "debugger": { "path": "echo.bat" }, + "testRunner": { "path": "echo.bat" }, + "schemaVersion" : "1.0", + "rootPath" : "." +} diff --git a/Fixtures/Miscellaneous/Errors/FatalErrorInSingleXCTest/TypeLibrary/.gitignore b/Fixtures/Miscellaneous/Errors/FatalErrorInSingleXCTest/TypeLibrary/.gitignore new file mode 100644 index 00000000000..0023a534063 --- /dev/null +++ b/Fixtures/Miscellaneous/Errors/FatalErrorInSingleXCTest/TypeLibrary/.gitignore @@ -0,0 +1,8 @@ +.DS_Store +/.build +/Packages +xcuserdata/ +DerivedData/ +.swiftpm/configuration/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc diff --git a/Fixtures/Miscellaneous/Errors/FatalErrorInSingleXCTest/TypeLibrary/Package.swift b/Fixtures/Miscellaneous/Errors/FatalErrorInSingleXCTest/TypeLibrary/Package.swift new file mode 100644 index 00000000000..65c9a6217d2 --- /dev/null +++ b/Fixtures/Miscellaneous/Errors/FatalErrorInSingleXCTest/TypeLibrary/Package.swift @@ -0,0 +1,24 @@ +// swift-tools-version:5.9 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "TypeLibrary", + products: [ + // Products define the executables and libraries a package produces, making them visible to other packages. + .library( + name: "TypeLibrary", + targets: ["TypeLibrary"]), + ], + targets: [ + // Targets are the basic building blocks of a package, defining a module or a test suite. + // Targets can depend on other targets in this package and products from dependencies. + .target( + name: "TypeLibrary"), + .testTarget( + name: "TypeLibraryTests", + dependencies: ["TypeLibrary"] + ), + ] +) diff --git a/Fixtures/Miscellaneous/Errors/FatalErrorInSingleXCTest/TypeLibrary/Sources/TypeLibrary/TypeLibrary.swift b/Fixtures/Miscellaneous/Errors/FatalErrorInSingleXCTest/TypeLibrary/Sources/TypeLibrary/TypeLibrary.swift new file mode 100644 index 00000000000..08b22b80fc0 --- /dev/null +++ b/Fixtures/Miscellaneous/Errors/FatalErrorInSingleXCTest/TypeLibrary/Sources/TypeLibrary/TypeLibrary.swift @@ -0,0 +1,2 @@ +// The Swift Programming Language +// https://docs.swift.org/swift-book diff --git a/Fixtures/Miscellaneous/Errors/FatalErrorInSingleXCTest/TypeLibrary/Tests/TypeLibraryTests/TypeLibraryTests.swift b/Fixtures/Miscellaneous/Errors/FatalErrorInSingleXCTest/TypeLibrary/Tests/TypeLibraryTests/TypeLibraryTests.swift new file mode 100644 index 00000000000..d0484f60f0f --- /dev/null +++ b/Fixtures/Miscellaneous/Errors/FatalErrorInSingleXCTest/TypeLibrary/Tests/TypeLibraryTests/TypeLibraryTests.swift @@ -0,0 +1,5 @@ +func testExample() throws { + let x = 0 + let y = 1 / x + print(y) +} diff --git a/Fixtures/Miscellaneous/MissingDependency/Bar/Package.swift b/Fixtures/Miscellaneous/MissingDependency/Bar/Package.swift index 6b5e7607d3f..aa073b16b03 100644 --- a/Fixtures/Miscellaneous/MissingDependency/Bar/Package.swift +++ b/Fixtures/Miscellaneous/MissingDependency/Bar/Package.swift @@ -4,7 +4,7 @@ import PackageDescription let package = Package( name: "Bar", dependencies: [ - .package(url: "../NonExistantPackage", from: "1.0.0"), + .package(url: "../NonExistentPackage", from: "1.0.0"), ], targets: [ .target(name: "Bar", path: "./"), diff --git a/Fixtures/Miscellaneous/PackageWithResource/Package.swift b/Fixtures/Miscellaneous/PackageWithResource/Package.swift new file mode 100644 index 00000000000..3ce2f26d6d4 --- /dev/null +++ b/Fixtures/Miscellaneous/PackageWithResource/Package.swift @@ -0,0 +1,10 @@ +// swift-tools-version:5.3 +import PackageDescription + +let package = Package( + name: "AwesomeResources", + targets: [ + .target(name: "AwesomeResources", resources: [.copy("hello.txt")]), + .testTarget(name: "AwesomeResourcesTest", dependencies: ["AwesomeResources"], resources: [.copy("world.txt")]) + ] +) diff --git a/Fixtures/Miscellaneous/PackageWithResource/Sources/AwesomeResources/AwesomeResource.swift b/Fixtures/Miscellaneous/PackageWithResource/Sources/AwesomeResources/AwesomeResource.swift new file mode 100644 index 00000000000..5e19132fb0f --- /dev/null +++ b/Fixtures/Miscellaneous/PackageWithResource/Sources/AwesomeResources/AwesomeResource.swift @@ -0,0 +1,6 @@ +import Foundation + +public struct AwesomeResource { + public init() {} + public let hello = try! String(contentsOf: Bundle.module.url(forResource: "hello", withExtension: "txt")!) +} diff --git a/Fixtures/Miscellaneous/PackageWithResource/Sources/AwesomeResources/hello.txt b/Fixtures/Miscellaneous/PackageWithResource/Sources/AwesomeResources/hello.txt new file mode 100644 index 00000000000..b6fc4c620b6 --- /dev/null +++ b/Fixtures/Miscellaneous/PackageWithResource/Sources/AwesomeResources/hello.txt @@ -0,0 +1 @@ +hello \ No newline at end of file diff --git a/Fixtures/Miscellaneous/PackageWithResource/Tests/AwesomeResourcesTest/MyTests.swift b/Fixtures/Miscellaneous/PackageWithResource/Tests/AwesomeResourcesTest/MyTests.swift new file mode 100644 index 00000000000..267d60c3e22 --- /dev/null +++ b/Fixtures/Miscellaneous/PackageWithResource/Tests/AwesomeResourcesTest/MyTests.swift @@ -0,0 +1,13 @@ +import XCTest +import Foundation +import AwesomeResources + +final class MyTests: XCTestCase { + func testFoo() { + XCTAssertTrue(AwesomeResource().hello == "hello") + } + func testBar() { + let world = try! String(contentsOf: Bundle.module.url(forResource: "world", withExtension: "txt")!) + XCTAssertTrue(world == "world") + } +} diff --git a/Fixtures/Miscellaneous/PackageWithResource/Tests/AwesomeResourcesTest/world.txt b/Fixtures/Miscellaneous/PackageWithResource/Tests/AwesomeResourcesTest/world.txt new file mode 100644 index 00000000000..04fea06420c --- /dev/null +++ b/Fixtures/Miscellaneous/PackageWithResource/Tests/AwesomeResourcesTest/world.txt @@ -0,0 +1 @@ +world \ No newline at end of file diff --git a/Fixtures/Miscellaneous/ParseAsLibrary/Package.swift b/Fixtures/Miscellaneous/ParseAsLibrary/Package.swift new file mode 100644 index 00000000000..e7ff15dddf6 --- /dev/null +++ b/Fixtures/Miscellaneous/ParseAsLibrary/Package.swift @@ -0,0 +1,15 @@ +// swift-tools-version: 6.0 + +import PackageDescription + +let package = Package( + name: "ParseAsLibrary", + products: [], + targets: [ + .executableTarget(name: "ExecutableTargetOneFileNamedMainMainAttr"), + .executableTarget(name: "ExecutableTargetOneFileNamedMainNoMainAttr"), + .executableTarget(name: "ExecutableTargetOneFileNotNamedMainMainAttr"), + .executableTarget(name: "ExecutableTargetOneFileNotNamedMainNoMainAttr"), + .executableTarget(name: "ExecutableTargetTwoFiles"), + ] +) diff --git a/Fixtures/Miscellaneous/ParseAsLibrary/Sources/ExecutableTargetOneFileNamedMainMainAttr/main.swift b/Fixtures/Miscellaneous/ParseAsLibrary/Sources/ExecutableTargetOneFileNamedMainMainAttr/main.swift new file mode 100644 index 00000000000..15b65931c58 --- /dev/null +++ b/Fixtures/Miscellaneous/ParseAsLibrary/Sources/ExecutableTargetOneFileNamedMainMainAttr/main.swift @@ -0,0 +1,3 @@ +@main struct Entry { + static func main() {} +} diff --git a/Fixtures/Miscellaneous/ParseAsLibrary/Sources/ExecutableTargetOneFileNamedMainNoMainAttr/main.swift b/Fixtures/Miscellaneous/ParseAsLibrary/Sources/ExecutableTargetOneFileNamedMainNoMainAttr/main.swift new file mode 100644 index 00000000000..517b47df53c --- /dev/null +++ b/Fixtures/Miscellaneous/ParseAsLibrary/Sources/ExecutableTargetOneFileNamedMainNoMainAttr/main.swift @@ -0,0 +1 @@ +print(42) diff --git a/Fixtures/Miscellaneous/ParseAsLibrary/Sources/ExecutableTargetOneFileNotNamedMainMainAttr/othername.swift b/Fixtures/Miscellaneous/ParseAsLibrary/Sources/ExecutableTargetOneFileNotNamedMainMainAttr/othername.swift new file mode 100644 index 00000000000..15b65931c58 --- /dev/null +++ b/Fixtures/Miscellaneous/ParseAsLibrary/Sources/ExecutableTargetOneFileNotNamedMainMainAttr/othername.swift @@ -0,0 +1,3 @@ +@main struct Entry { + static func main() {} +} diff --git a/Fixtures/Miscellaneous/ParseAsLibrary/Sources/ExecutableTargetOneFileNotNamedMainNoMainAttr/othername.swift b/Fixtures/Miscellaneous/ParseAsLibrary/Sources/ExecutableTargetOneFileNotNamedMainNoMainAttr/othername.swift new file mode 100644 index 00000000000..517b47df53c --- /dev/null +++ b/Fixtures/Miscellaneous/ParseAsLibrary/Sources/ExecutableTargetOneFileNotNamedMainNoMainAttr/othername.swift @@ -0,0 +1 @@ +print(42) diff --git a/Fixtures/Miscellaneous/ParseAsLibrary/Sources/ExecutableTargetTwoFiles/one.swift b/Fixtures/Miscellaneous/ParseAsLibrary/Sources/ExecutableTargetTwoFiles/one.swift new file mode 100644 index 00000000000..15b65931c58 --- /dev/null +++ b/Fixtures/Miscellaneous/ParseAsLibrary/Sources/ExecutableTargetTwoFiles/one.swift @@ -0,0 +1,3 @@ +@main struct Entry { + static func main() {} +} diff --git a/Fixtures/Miscellaneous/ParseAsLibrary/Sources/ExecutableTargetTwoFiles/two.swift b/Fixtures/Miscellaneous/ParseAsLibrary/Sources/ExecutableTargetTwoFiles/two.swift new file mode 100644 index 00000000000..d29f0705072 --- /dev/null +++ b/Fixtures/Miscellaneous/ParseAsLibrary/Sources/ExecutableTargetTwoFiles/two.swift @@ -0,0 +1 @@ +func foo() {} diff --git a/Fixtures/Miscellaneous/PluginGeneratedResources/Package.swift b/Fixtures/Miscellaneous/PluginGeneratedResources/Package.swift index 8e2b4359b3c..e6a409c1a74 100644 --- a/Fixtures/Miscellaneous/PluginGeneratedResources/Package.swift +++ b/Fixtures/Miscellaneous/PluginGeneratedResources/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version: 5.7 +// swift-tools-version: 6.0 import PackageDescription diff --git a/Fixtures/Miscellaneous/PluginGeneratedResources/Plugins/Generator/plugin.swift b/Fixtures/Miscellaneous/PluginGeneratedResources/Plugins/Generator/plugin.swift index abf10eeb159..c842ea6ae5b 100644 --- a/Fixtures/Miscellaneous/PluginGeneratedResources/Plugins/Generator/plugin.swift +++ b/Fixtures/Miscellaneous/PluginGeneratedResources/Plugins/Generator/plugin.swift @@ -1,4 +1,16 @@ import PackagePlugin +import Foundation + +#if os(Android) +let touchExe = "/system/bin/touch" +let touchArgs: [String] = [] +#elseif os(Windows) +let touchExe = "C:/Windows/System32/cmd.exe" +let touchArgs = ["/c", "copy", "NUL"] +#else +let touchExe = "/usr/bin/touch" +let touchArgs: [String] = [] +#endif @main struct GeneratorPlugin: BuildToolPlugin { @@ -6,9 +18,9 @@ struct GeneratorPlugin: BuildToolPlugin { return [ .prebuildCommand( displayName: "Generating empty file", - executable: .init("/usr/bin/touch"), - arguments: [context.pluginWorkDirectory.appending("best.txt")], - outputFilesDirectory: context.pluginWorkDirectory + executable: .init(fileURLWithPath: touchExe), + arguments: touchArgs + [String(cString: (context.pluginWorkDirectoryURL.appending(path: "best.txt") as NSURL).fileSystemRepresentation)], + outputFilesDirectory: context.pluginWorkDirectoryURL ) ] } diff --git a/Fixtures/Miscellaneous/Plugins/BinaryTargetExePlugin/Dependency/MyBinaryTargetExeArtifactBundle.artifactbundle/info.json b/Fixtures/Miscellaneous/Plugins/BinaryTargetExePlugin/Dependency/MyBinaryTargetExeArtifactBundle.artifactbundle/info.json new file mode 100644 index 00000000000..322cdaa8f4a --- /dev/null +++ b/Fixtures/Miscellaneous/Plugins/BinaryTargetExePlugin/Dependency/MyBinaryTargetExeArtifactBundle.artifactbundle/info.json @@ -0,0 +1,19 @@ +{ + "schemaVersion": "1.0", + "artifacts": { + "mytool": { + "type": "executable", + "version": "1.2.3", + "variants": [ + { + "path": "mytool-macos/mytool", + "supportedTriples": ["x86_64-apple-macosx", "arm64-apple-macosx"] + }, + { + "path": "mytool-linux/mytool", + "supportedTriples": ["x86_64-unknown-linux-gnu"] + } + ] + } + } +} diff --git a/Fixtures/Miscellaneous/Plugins/BinaryTargetExePlugin/Dependency/MyBinaryTargetExeArtifactBundle.artifactbundle/mytool-linux/mytool b/Fixtures/Miscellaneous/Plugins/BinaryTargetExePlugin/Dependency/MyBinaryTargetExeArtifactBundle.artifactbundle/mytool-linux/mytool new file mode 100755 index 00000000000..bc45801290f --- /dev/null +++ b/Fixtures/Miscellaneous/Plugins/BinaryTargetExePlugin/Dependency/MyBinaryTargetExeArtifactBundle.artifactbundle/mytool-linux/mytool @@ -0,0 +1,29 @@ +#!/bin/bash + +print_usage() { + echo "usage: ${0##*/} [--verbose] " +} + +# Parse arguments until we find '--' or an argument that isn't an option. +until [ $# -eq 0 ] +do + case "$1" in + --verbose) verbose=1; shift;; + --) shift; break;; + -*) echo "unknown option: ${1}"; print_usage; exit 1; shift;; + *) break;; + esac +done + +# Print usage and leave if we don't have exactly two arguments. +if [ $# -ne 2 ]; then + print_usage + exit 1 +fi + +# For our sample tool we just copy from one to the other. +if [ $verbose != 0 ]; then + echo "[${0##*/}-linux] '$1' '$2'" +fi + +cp "$1" "$2" diff --git a/Fixtures/Miscellaneous/Plugins/BinaryTargetExePlugin/Dependency/MyBinaryTargetExeArtifactBundle.artifactbundle/mytool-macos/mytool b/Fixtures/Miscellaneous/Plugins/BinaryTargetExePlugin/Dependency/MyBinaryTargetExeArtifactBundle.artifactbundle/mytool-macos/mytool new file mode 100755 index 00000000000..38d637c5f8e --- /dev/null +++ b/Fixtures/Miscellaneous/Plugins/BinaryTargetExePlugin/Dependency/MyBinaryTargetExeArtifactBundle.artifactbundle/mytool-macos/mytool @@ -0,0 +1,29 @@ +#!/bin/bash + +print_usage() { + echo "usage: ${0##*/} [--verbose] " +} + +# Parse arguments until we find '--' or an argument that isn't an option. +until [ $# -eq 0 ] +do + case "$1" in + --verbose) verbose=1; shift;; + --) shift; break;; + -*) echo "unknown option: ${1}"; print_usage; exit 1; shift;; + *) break;; + esac +done + +# Print usage and leave if we don't have exactly two arguments. +if [ $# -ne 2 ]; then + print_usage + exit 1 +fi + +# For our sample tool we just copy from one to the other. +if [ $verbose != 0 ]; then + echo "[${0##*/}-macosx] '$1' '$2'" +fi + +cp "$1" "$2" diff --git a/Fixtures/Miscellaneous/Plugins/BinaryTargetExePlugin/Package.swift b/Fixtures/Miscellaneous/Plugins/BinaryTargetExePlugin/Package.swift new file mode 100644 index 00000000000..d240b1f4ef0 --- /dev/null +++ b/Fixtures/Miscellaneous/Plugins/BinaryTargetExePlugin/Package.swift @@ -0,0 +1,38 @@ +// swift-tools-version: 5.6 +import PackageDescription + +let package = Package( + name: "MyBinaryTargetExePlugin", + + products: [ + .executable( + name: "MyPluginExe", + targets: ["MyPluginExe"] + ), + .plugin( + name: "MyPlugin", + targets: ["MyPlugin"] + ), + .executable( + name: "MyBinaryTargetExe", + targets: ["MyBinaryTargetExeArtifactBundle"] + ), + ], + targets: [ + .executableTarget( + name: "MyPluginExe", + dependencies: [], + exclude: [], + ), + + .plugin( + name: "MyPlugin", + capability: .buildTool(), + dependencies: ["MyPluginExe", "MyBinaryTargetExeArtifactBundle"] + ), + .binaryTarget( + name: "MyBinaryTargetExeArtifactBundle", + path: "Dependency/MyBinaryTargetExeArtifactBundle.artifactbundle" + ), + ] +) diff --git a/Fixtures/Miscellaneous/Plugins/BinaryTargetExePlugin/Plugins/MyPlugin/plugin.swift b/Fixtures/Miscellaneous/Plugins/BinaryTargetExePlugin/Plugins/MyPlugin/plugin.swift new file mode 100644 index 00000000000..8b446c5634d --- /dev/null +++ b/Fixtures/Miscellaneous/Plugins/BinaryTargetExePlugin/Plugins/MyPlugin/plugin.swift @@ -0,0 +1,9 @@ +import PackagePlugin + +@main +struct MyPlugin: BuildToolPlugin { + + func createBuildCommands(context: PluginContext, target: Target) throws -> [Command] { + print("Hello from MyPlugin!") + } +} diff --git a/Fixtures/Miscellaneous/Plugins/BinaryTargetExePlugin/Sources/MyPluginExe/main.swift b/Fixtures/Miscellaneous/Plugins/BinaryTargetExePlugin/Sources/MyPluginExe/main.swift new file mode 100644 index 00000000000..d64011ba57e --- /dev/null +++ b/Fixtures/Miscellaneous/Plugins/BinaryTargetExePlugin/Sources/MyPluginExe/main.swift @@ -0,0 +1 @@ +print("It's Me MyPluginExe\n") \ No newline at end of file diff --git a/Fixtures/Miscellaneous/Plugins/BuildToolPluginCompilationError/Package.swift b/Fixtures/Miscellaneous/Plugins/BuildToolPluginCompilationError/Package.swift new file mode 100644 index 00000000000..e3eb90f3a29 --- /dev/null +++ b/Fixtures/Miscellaneous/Plugins/BuildToolPluginCompilationError/Package.swift @@ -0,0 +1,17 @@ +// swift-tools-version: 5.6 +import PackageDescription +let package = Package( + name: "MyPackage", + targets: [ + .target( + name: "MyLibrary", + plugins: [ + "MyPlugin", + ] + ), + .plugin( + name: "MyPlugin", + capability: .buildTool() + ), + ] +) diff --git a/Fixtures/Miscellaneous/Plugins/BuildToolPluginCompilationError/Plugins/MyPlugin/plugin.swift b/Fixtures/Miscellaneous/Plugins/BuildToolPluginCompilationError/Plugins/MyPlugin/plugin.swift new file mode 100644 index 00000000000..550b8c22a65 --- /dev/null +++ b/Fixtures/Miscellaneous/Plugins/BuildToolPluginCompilationError/Plugins/MyPlugin/plugin.swift @@ -0,0 +1,15 @@ +import PackagePlugin +import Foundation +@main +struct MyBuildToolPlugin: BuildToolPlugin { + func createBuildCommands( + context: PluginContext, + target: Target + ) throws -> [Command] { + print("This is text from the plugin") + throw "This is an error from the plugin" + return [] + } + +} +extension String : Error {} diff --git a/Fixtures/Miscellaneous/Plugins/BuildToolPluginCompilationError/Sources/MyLibrary/library.swift b/Fixtures/Miscellaneous/Plugins/BuildToolPluginCompilationError/Sources/MyLibrary/library.swift new file mode 100644 index 00000000000..ed191a40c0e --- /dev/null +++ b/Fixtures/Miscellaneous/Plugins/BuildToolPluginCompilationError/Sources/MyLibrary/library.swift @@ -0,0 +1 @@ +public func Foo() { } diff --git a/Fixtures/Miscellaneous/Plugins/CommandPluginCompilationError/Package.swift b/Fixtures/Miscellaneous/Plugins/CommandPluginCompilationError/Package.swift new file mode 100644 index 00000000000..820c7a6d89e --- /dev/null +++ b/Fixtures/Miscellaneous/Plugins/CommandPluginCompilationError/Package.swift @@ -0,0 +1,34 @@ +// swift-tools-version: 5.6 +import PackageDescription +let package = Package( + name: "MyPackage", + products: [ + .library( + name: "MyLibrary", + targets: ["MyLibrary"] + ), + .executable( + name: "MyExecutable", + targets: ["MyExecutable"] + ), + ], + targets: [ + .target( + name: "MyLibrary" + ), + .executableTarget( + name: "MyExecutable", + dependencies: ["MyLibrary"] + ), + .plugin( + name: "MyBuildToolPlugin", + capability: .buildTool() + ), + .plugin( + name: "MyCommandPlugin", + capability: .command( + intent: .custom(verb: "my-build-tester", description: "Help description") + ) + ), + ] +) diff --git a/Fixtures/Miscellaneous/Plugins/CommandPluginCompilationError/Plugins/MyBuildToolPlugin/plugin.swift b/Fixtures/Miscellaneous/Plugins/CommandPluginCompilationError/Plugins/MyBuildToolPlugin/plugin.swift new file mode 100644 index 00000000000..0e674b46988 --- /dev/null +++ b/Fixtures/Miscellaneous/Plugins/CommandPluginCompilationError/Plugins/MyBuildToolPlugin/plugin.swift @@ -0,0 +1,9 @@ +import PackagePlugin +@main struct MyBuildToolPlugin: BuildToolPlugin { + func createBuildCommands( + context: PluginContext, + target: Target + ) throws -> [Command] { + return [] + } +} diff --git a/Fixtures/Miscellaneous/Plugins/CommandPluginCompilationError/Plugins/MyCommandPlugin/plugin.swift b/Fixtures/Miscellaneous/Plugins/CommandPluginCompilationError/Plugins/MyCommandPlugin/plugin.swift new file mode 100644 index 00000000000..8704377cba5 --- /dev/null +++ b/Fixtures/Miscellaneous/Plugins/CommandPluginCompilationError/Plugins/MyCommandPlugin/plugin.swift @@ -0,0 +1,9 @@ +import PackagePlugin +@main struct MyCommandPlugin: CommandPlugin { + func performCommand( + context: PluginContext, + arguments: [String] + ) throws { + this is an error + } +} diff --git a/Fixtures/Miscellaneous/Plugins/CommandPluginCompilationError/Sources/MyExecutable/main.swift b/Fixtures/Miscellaneous/Plugins/CommandPluginCompilationError/Sources/MyExecutable/main.swift new file mode 100644 index 00000000000..1c9c151f5f6 --- /dev/null +++ b/Fixtures/Miscellaneous/Plugins/CommandPluginCompilationError/Sources/MyExecutable/main.swift @@ -0,0 +1,2 @@ +import MyLibrary +print("\\(GetGreeting()), World!") diff --git a/Fixtures/Miscellaneous/Plugins/CommandPluginCompilationError/Sources/MyLibrary/library.swift b/Fixtures/Miscellaneous/Plugins/CommandPluginCompilationError/Sources/MyLibrary/library.swift new file mode 100644 index 00000000000..4ce3fd140d4 --- /dev/null +++ b/Fixtures/Miscellaneous/Plugins/CommandPluginCompilationError/Sources/MyLibrary/library.swift @@ -0,0 +1 @@ +public func GetGreeting() -> String { return "Hello" } diff --git a/Fixtures/Miscellaneous/Plugins/CommandPluginTestStub/Package.swift b/Fixtures/Miscellaneous/Plugins/CommandPluginTestStub/Package.swift new file mode 100644 index 00000000000..521f9a0a51c --- /dev/null +++ b/Fixtures/Miscellaneous/Plugins/CommandPluginTestStub/Package.swift @@ -0,0 +1,55 @@ +// swift-tools-version: 5.9 + +import PackageDescription + +let package = Package( + name: "CommandPluginDiagnostics", + targets: [ + .plugin( + name: "diagnostics-stub", + capability: .command(intent: .custom( + verb: "print-diagnostics", + description: "Writes diagnostic messages for testing" + )) + ), + .plugin( + name: "targetbuild-stub", + capability: .command(intent: .custom( + verb: "build-target", + description: "Build a target for testing" + )) + ), + .plugin( + name: "plugin-dependencies-stub", + capability: .command(intent: .custom( + verb: "build-plugin-dependency", + description: "Build a plugin dependency for testing" + )), + dependencies: [ + .target(name: "plugintool") + ] + ), + .plugin( + name: "check-testability", + capability: .command(intent: .custom( + verb: "check-testability", + description: "Check testability of a target" + )) + ), + .executableTarget( + name: "placeholder" + ), + .executableTarget( + name: "plugintool" + ), + .target( + name: "InternalModule" + ), + .testTarget( + name: "InternalModuleTests", + dependencies: [ + .target(name: "InternalModule") + ] + ), + ] +) diff --git a/Fixtures/Miscellaneous/Plugins/CommandPluginTestStub/Plugins/check-testability/main.swift b/Fixtures/Miscellaneous/Plugins/CommandPluginTestStub/Plugins/check-testability/main.swift new file mode 100644 index 00000000000..6aba851ddd4 --- /dev/null +++ b/Fixtures/Miscellaneous/Plugins/CommandPluginTestStub/Plugins/check-testability/main.swift @@ -0,0 +1,45 @@ +import Foundation +import PackagePlugin + +@main +struct CheckTestability: CommandPlugin { + // This is a helper for testing target builds to ensure that they are testable. + func performCommand(context: PluginContext, arguments: [String]) async throws { + // Parse the arguments: + guard arguments.count == 3 else { + fatalError("Usage: ") + } + let rawSubsetName = arguments[0] + var subset: PackageManager.BuildSubset + switch rawSubsetName { + // Special subset names + case "all-with-tests": + subset = .all(includingTests: true) + // By default, treat the subset as a target name + default: + subset = .target(rawSubsetName) + } + guard let config = PackageManager.BuildConfiguration(rawValue: arguments[1]) else { + fatalError("Invalid configuration: \(arguments[1])") + } + let shouldTestable = arguments[2] == "true" + + var parameters = PackageManager.BuildParameters() + parameters.configuration = config + parameters.logging = .verbose + + // Perform the build + let result = try packageManager.build(subset, parameters: parameters) + + // Check if the build was successful + guard result.succeeded else { + fatalError("Build failed: \(result.logText)") + } + + // Check if the build log contains "-enable-testing" flag + let isTestable = result.logText.contains("-enable-testing") + if isTestable != shouldTestable { + fatalError("Testability mismatch: expected \(shouldTestable), but got \(isTestable):\n\(result.logText)") + } + } +} diff --git a/Fixtures/Miscellaneous/Plugins/CommandPluginTestStub/Plugins/diagnostics-stub/diagnostics_stub.swift b/Fixtures/Miscellaneous/Plugins/CommandPluginTestStub/Plugins/diagnostics-stub/diagnostics_stub.swift new file mode 100644 index 00000000000..25d49256f1e --- /dev/null +++ b/Fixtures/Miscellaneous/Plugins/CommandPluginTestStub/Plugins/diagnostics-stub/diagnostics_stub.swift @@ -0,0 +1,52 @@ +import Foundation +import PackagePlugin + +@main +struct diagnostics_stub: CommandPlugin { + // This is a helper for testing plugin diagnostics. It sends different messages to SwiftPM depending on its arguments. + func performCommand(context: PluginContext, arguments: [String]) async throws { + // Build a target, possibly asking SwiftPM to echo the logs as they are produced. + if arguments.contains("build") { + // If echoLogs is true, SwiftPM will print build logs to stderr as they are produced. + // SwiftPM does not add a prefix to these logs. + let result = try packageManager.build( + .product("placeholder"), + parameters: .init(echoLogs: arguments.contains("echologs")) + ) + + // To verify that logs are also returned correctly to the plugin, + // print the accumulated log buffer lines with a prefix to + // distinguish them from echoed logs. These logs are normal output + // from the plugin and will be printed on stdout. + if arguments.contains("printlogs") { + for line in result.logText.components(separatedBy: "\n") { + print("command plugin: packageManager.build logtext: \(line)") + } + } + } + + // Anything a plugin writes to standard output appears on standard output. + // Printing to stderr will also go to standard output because SwiftPM combines + // stdout and stderr before launching the plugin. + if arguments.contains("print") { + print("command plugin: print") + } + + // Diagnostics are collected by SwiftPM and printed to standard error, depending on the current log verbosity level. + if arguments.contains("progress") { + Diagnostics.progress("command plugin: Diagnostics.progress") // prefixed with [plugin_name] + } + + if arguments.contains("remark") { + Diagnostics.remark("command plugin: Diagnostics.remark") // prefixed with 'info:' when printed + } + + if arguments.contains("warning") { + Diagnostics.warning("command plugin: Diagnostics.warning") // prefixed with 'warning:' when printed + } + + if arguments.contains("error") { + Diagnostics.error("command plugin: Diagnostics.error") // prefixed with 'error:' when printed + } + } +} diff --git a/Fixtures/Miscellaneous/Plugins/CommandPluginTestStub/Plugins/plugin-dependencies-stub/main.swift b/Fixtures/Miscellaneous/Plugins/CommandPluginTestStub/Plugins/plugin-dependencies-stub/main.swift new file mode 100644 index 00000000000..7034dbc528b --- /dev/null +++ b/Fixtures/Miscellaneous/Plugins/CommandPluginTestStub/Plugins/plugin-dependencies-stub/main.swift @@ -0,0 +1,13 @@ +import PackagePlugin + +@main +struct test: CommandPlugin { + // This plugin exists to test that the executable it requires is built correctly when cross-compiling + func performCommand(context: PluginContext, arguments: [String]) async throws { + print("Hello from dependencies-stub") + let _ = try packageManager.build( + .product("placeholder"), + parameters: .init(configuration: .debug, logging: .concise) + ) + } +} \ No newline at end of file diff --git a/Fixtures/Miscellaneous/Plugins/CommandPluginTestStub/Plugins/targetbuild-stub/targetbuild_stub.swift b/Fixtures/Miscellaneous/Plugins/CommandPluginTestStub/Plugins/targetbuild-stub/targetbuild_stub.swift new file mode 100644 index 00000000000..84d07337066 --- /dev/null +++ b/Fixtures/Miscellaneous/Plugins/CommandPluginTestStub/Plugins/targetbuild-stub/targetbuild_stub.swift @@ -0,0 +1,25 @@ +import Foundation +import PackagePlugin + +@main +struct targetbuild_stub: CommandPlugin { + // This is a helper for testing target builds performed on behalf of plugins. + // It sends asks SwiftPM to build a target with different options depending on its arguments. + func performCommand(context: PluginContext, arguments: [String]) async throws { + // Build a target + var parameters = PackageManager.BuildParameters() + if arguments.contains("build-debug") { + parameters.configuration = .debug + } else if arguments.contains("build-release") { + parameters.configuration = .release + } else if arguments.contains("build-inherit") { + parameters.configuration = .inherit + } + // If no 'build-*' argument is present, the default (.debug) will be used. + + let _ = try packageManager.build( + .product("placeholder"), + parameters: parameters + ) + } +} \ No newline at end of file diff --git a/Fixtures/Miscellaneous/Plugins/CommandPluginTestStub/Sources/InternalModule/InternalModule.swift b/Fixtures/Miscellaneous/Plugins/CommandPluginTestStub/Sources/InternalModule/InternalModule.swift new file mode 100644 index 00000000000..3485b5f996a --- /dev/null +++ b/Fixtures/Miscellaneous/Plugins/CommandPluginTestStub/Sources/InternalModule/InternalModule.swift @@ -0,0 +1,3 @@ +internal func internalFunction() { + print("Internal function") +} diff --git a/Fixtures/Miscellaneous/Plugins/CommandPluginTestStub/Sources/placeholder/main.swift b/Fixtures/Miscellaneous/Plugins/CommandPluginTestStub/Sources/placeholder/main.swift new file mode 100644 index 00000000000..e59623db380 --- /dev/null +++ b/Fixtures/Miscellaneous/Plugins/CommandPluginTestStub/Sources/placeholder/main.swift @@ -0,0 +1,4 @@ +// The Swift Programming Language +// https://docs.swift.org/swift-book + +print("Hello, world from executable target!") diff --git a/Fixtures/Miscellaneous/Plugins/CommandPluginTestStub/Sources/plugintool/main.swift b/Fixtures/Miscellaneous/Plugins/CommandPluginTestStub/Sources/plugintool/main.swift new file mode 100644 index 00000000000..3d2f1a1f757 --- /dev/null +++ b/Fixtures/Miscellaneous/Plugins/CommandPluginTestStub/Sources/plugintool/main.swift @@ -0,0 +1 @@ +print("Hello from plugintool") diff --git a/Fixtures/Miscellaneous/Plugins/CommandPluginTestStub/Tests/InternalModuleTests/InternalModuleTests.swift b/Fixtures/Miscellaneous/Plugins/CommandPluginTestStub/Tests/InternalModuleTests/InternalModuleTests.swift new file mode 100644 index 00000000000..828e3df6838 --- /dev/null +++ b/Fixtures/Miscellaneous/Plugins/CommandPluginTestStub/Tests/InternalModuleTests/InternalModuleTests.swift @@ -0,0 +1 @@ +@testable import InternalModule diff --git a/Fixtures/Miscellaneous/Plugins/DependentPlugins/Package.swift b/Fixtures/Miscellaneous/Plugins/DependentPlugins/Package.swift new file mode 100644 index 00000000000..617ae6132a8 --- /dev/null +++ b/Fixtures/Miscellaneous/Plugins/DependentPlugins/Package.swift @@ -0,0 +1,39 @@ +// swift-tools-version: 6.0 + +import PackageDescription + +let package = Package( + name: "DependentPlugins", + platforms: [ .macOS(.v13) ], + dependencies: [ + ], + targets: [ + .executableTarget(name: "MyExecutable"), + .executableTarget(name: "MyExecutable2"), + + .plugin( + name: "MyPlugin", + capability: .buildTool(), + dependencies: [ + "MyExecutable" + ] + ), + + .plugin( + name: "MyPlugin2", + capability: .buildTool(), + dependencies: [ + "MyExecutable2" + ] + ), + + .executableTarget( + name: "MyClient", + plugins: [ + "MyPlugin", + "MyPlugin2", + ] + ), + ], + swiftLanguageModes: [.v5] +) diff --git a/Fixtures/Miscellaneous/Plugins/DependentPlugins/Plugins/MyPlugin/plugin.swift b/Fixtures/Miscellaneous/Plugins/DependentPlugins/Plugins/MyPlugin/plugin.swift new file mode 100644 index 00000000000..9ae905661ea --- /dev/null +++ b/Fixtures/Miscellaneous/Plugins/DependentPlugins/Plugins/MyPlugin/plugin.swift @@ -0,0 +1,16 @@ +import PackagePlugin + +@main +struct MyPlugin: BuildToolPlugin { + func createBuildCommands(context: PluginContext, target: Target) async throws -> [Command] { + let outputFilePath = context.pluginWorkDirectory.appending("MyGeneratedFile.swift") + return [ + .buildCommand( + displayName: "Running MyExecutable", + executable: try context.tool(named: "MyExecutable").path, + arguments: ["--output-file-path", outputFilePath.string], + outputFiles: [outputFilePath] + ) + ] + } +} diff --git a/Fixtures/Miscellaneous/Plugins/DependentPlugins/Plugins/MyPlugin2/plugin.swift b/Fixtures/Miscellaneous/Plugins/DependentPlugins/Plugins/MyPlugin2/plugin.swift new file mode 100644 index 00000000000..b2134aee34a --- /dev/null +++ b/Fixtures/Miscellaneous/Plugins/DependentPlugins/Plugins/MyPlugin2/plugin.swift @@ -0,0 +1,28 @@ +import PackagePlugin +import Foundation + +@main +struct MyPlugin: BuildToolPlugin { + func createBuildCommands(context: PluginContext, target: Target) async throws -> [Command] { + guard let sourceTarget = target as? SourceModuleTarget else { + throw "Target is not a source target, cannot get a file list" + } + guard let inputFilePath = sourceTarget.pluginGeneratedSources.first(where: { $0.lastPathComponent == "MyGeneratedFile.swift" })?.path else { + throw "Cannot find MyGeneratedFile.swift, files: \(sourceTarget.pluginGeneratedSources), target: \(target)" + } + return [ + .buildCommand( + displayName: "Running MyExecutable2", + executable: try context.tool(named: "MyExecutable2").path, + arguments: ["--input-file-path", inputFilePath], + inputFiles: [Path(inputFilePath)] + ) + ] + } +} + +extension String: Error, LocalizedError { + public var errorDescription: String? { + self + } +} diff --git a/Fixtures/Miscellaneous/Plugins/DependentPlugins/Sources/MyClient/client.swift b/Fixtures/Miscellaneous/Plugins/DependentPlugins/Sources/MyClient/client.swift new file mode 100644 index 00000000000..2b522f38b32 --- /dev/null +++ b/Fixtures/Miscellaneous/Plugins/DependentPlugins/Sources/MyClient/client.swift @@ -0,0 +1,6 @@ +@main +struct Client { + static func main() throws { + print(MyGeneratedStruct.message) + } +} diff --git a/Fixtures/Miscellaneous/Plugins/DependentPlugins/Sources/MyExecutable/tool.swift b/Fixtures/Miscellaneous/Plugins/DependentPlugins/Sources/MyExecutable/tool.swift new file mode 100644 index 00000000000..d319ed9bd67 --- /dev/null +++ b/Fixtures/Miscellaneous/Plugins/DependentPlugins/Sources/MyExecutable/tool.swift @@ -0,0 +1,26 @@ +import Foundation + +@main +struct Tool { + static func main() async throws { + print("warning: Whoops! Coming from the executable", to: &StdErr.shared) + + let path = CommandLine.arguments[2] + print("Writing a file to \(path)") + + try #""" + public struct MyGeneratedStruct { + public static var message: String = "You got struct'd" + } + """#.write(to: URL(fileURLWithPath: path), atomically: true, encoding: .utf8) + } +} + +struct StdErr: TextOutputStream { + static var shared: Self = .init() + mutating func write(_ string: String) { + string.withCString { ptr in + _ = fputs(ptr, stderr) + } + } +} diff --git a/Fixtures/Miscellaneous/Plugins/DependentPlugins/Sources/MyExecutable2/tool.swift b/Fixtures/Miscellaneous/Plugins/DependentPlugins/Sources/MyExecutable2/tool.swift new file mode 100644 index 00000000000..03529454229 --- /dev/null +++ b/Fixtures/Miscellaneous/Plugins/DependentPlugins/Sources/MyExecutable2/tool.swift @@ -0,0 +1,10 @@ +import Foundation + +@main +struct Tool { + static func main() async throws { + let path = CommandLine.arguments[2] + print("Printing file at \(path)") + print(try String(contentsOfFile: path)) + } +} diff --git a/Fixtures/Miscellaneous/Plugins/MyBinaryToolPlugin/Binaries/MyVendedSourceGenBuildTool.artifactbundle/info.json b/Fixtures/Miscellaneous/Plugins/MyBinaryToolPlugin/Binaries/MyVendedSourceGenBuildTool.artifactbundle/info.json index 322cdaa8f4a..d64be1aaa42 100644 --- a/Fixtures/Miscellaneous/Plugins/MyBinaryToolPlugin/Binaries/MyVendedSourceGenBuildTool.artifactbundle/info.json +++ b/Fixtures/Miscellaneous/Plugins/MyBinaryToolPlugin/Binaries/MyVendedSourceGenBuildTool.artifactbundle/info.json @@ -12,6 +12,10 @@ { "path": "mytool-linux/mytool", "supportedTriples": ["x86_64-unknown-linux-gnu"] + }, + { + "path": "mytool-windows/mytool.bat", + "supportedTriples": ["x86_64-unknown-windows-msvc", "aarch64-unknown-windows-msvc"] } ] } diff --git a/Fixtures/Miscellaneous/Plugins/MyBinaryToolPlugin/Binaries/MyVendedSourceGenBuildTool.artifactbundle/mytool-windows/mytool.bat b/Fixtures/Miscellaneous/Plugins/MyBinaryToolPlugin/Binaries/MyVendedSourceGenBuildTool.artifactbundle/mytool-windows/mytool.bat new file mode 100644 index 00000000000..50f66dd2c73 --- /dev/null +++ b/Fixtures/Miscellaneous/Plugins/MyBinaryToolPlugin/Binaries/MyVendedSourceGenBuildTool.artifactbundle/mytool-windows/mytool.bat @@ -0,0 +1,21 @@ +@echo off + +set verbose=false +IF NOT "%1"=="" ( + IF "%1"=="--verbose" ( + SET verbose=true + SHIFT + ) +) + +set input=%1 +set output=%2 +shift +shift + + +if "%verbose%" == "true" ( + echo "[mytool-windows] '%input%' '%output%'" +) +@echo on +echo f | xcopy.exe /f "%input%" "%output%" \ No newline at end of file diff --git a/Fixtures/Miscellaneous/Plugins/MyBinaryToolPlugin/Package.swift b/Fixtures/Miscellaneous/Plugins/MyBinaryToolPlugin/Package.swift index 78fc40d0662..4d603e1eb67 100644 --- a/Fixtures/Miscellaneous/Plugins/MyBinaryToolPlugin/Package.swift +++ b/Fixtures/Miscellaneous/Plugins/MyBinaryToolPlugin/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version: 5.6 +// swift-tools-version: 6.1 import PackageDescription let package = Package( diff --git a/Fixtures/Miscellaneous/Plugins/MyBinaryToolPlugin/Plugins/MySourceGenBuildToolPlugin/plugin.swift b/Fixtures/Miscellaneous/Plugins/MyBinaryToolPlugin/Plugins/MySourceGenBuildToolPlugin/plugin.swift index 0d9066674c5..20a14f392f9 100644 --- a/Fixtures/Miscellaneous/Plugins/MyBinaryToolPlugin/Plugins/MySourceGenBuildToolPlugin/plugin.swift +++ b/Fixtures/Miscellaneous/Plugins/MyBinaryToolPlugin/Plugins/MySourceGenBuildToolPlugin/plugin.swift @@ -7,6 +7,8 @@ struct MyPlugin: BuildToolPlugin { print("Hello from the Build Tool Plugin!") guard let target = target as? SourceModuleTarget else { return [] } let inputFiles = target.sourceFiles.filter({ $0.path.extension == "dat" }) + let workDir = context.pluginWorkDirectoryURL + return try inputFiles.map { let inputFile = $0 let inputPath = inputFile.path @@ -29,6 +31,16 @@ struct MyPlugin: BuildToolPlugin { outputPath ] ) - } + } + [ + .prebuildCommand( + displayName: + "Generating files in \(workDir.path)", + executable: + try context.tool(named: "mytool").url, + arguments: + ["--verbose", "\(target.directoryURL.appendingPathComponent("bar.in").path)", "\(workDir.appendingPathComponent("bar.swift").path)"], + outputFilesDirectory: workDir + ) + ] } } diff --git a/Fixtures/Miscellaneous/Plugins/MyBinaryToolPlugin/Sources/MyLocalTool/bar.in b/Fixtures/Miscellaneous/Plugins/MyBinaryToolPlugin/Sources/MyLocalTool/bar.in new file mode 100644 index 00000000000..361a7edf3f6 --- /dev/null +++ b/Fixtures/Miscellaneous/Plugins/MyBinaryToolPlugin/Sources/MyLocalTool/bar.in @@ -0,0 +1 @@ +let bar = "I am Bar!" diff --git a/Fixtures/Miscellaneous/Plugins/MyBinaryToolPlugin/Sources/MyLocalTool/main.swift b/Fixtures/Miscellaneous/Plugins/MyBinaryToolPlugin/Sources/MyLocalTool/main.swift index 1bbab0f2fc3..cfe57e9db30 100644 --- a/Fixtures/Miscellaneous/Plugins/MyBinaryToolPlugin/Sources/MyLocalTool/main.swift +++ b/Fixtures/Miscellaneous/Plugins/MyBinaryToolPlugin/Sources/MyLocalTool/main.swift @@ -1 +1,2 @@ print("Generated string Foo: '\(foo)'") +print("Generated string Bar: '\(bar)'") diff --git a/Fixtures/Miscellaneous/Plugins/MyBuildToolPluginDependencies/Sources/MySourceGenBuildTool/main.swift b/Fixtures/Miscellaneous/Plugins/MyBuildToolPluginDependencies/Sources/MySourceGenBuildTool/main.swift index 94f7766628c..bedca50b459 100644 --- a/Fixtures/Miscellaneous/Plugins/MyBuildToolPluginDependencies/Sources/MySourceGenBuildTool/main.swift +++ b/Fixtures/Miscellaneous/Plugins/MyBuildToolPluginDependencies/Sources/MySourceGenBuildTool/main.swift @@ -13,6 +13,6 @@ let variableName = URL(fileURLWithPath: inputFile).deletingPathExtension().lastP let inputData = FileManager.default.contents(atPath: inputFile) ?? Data() let dataAsHex = inputData.map { String(format: "%02hhx", $0) }.joined() -let outputString = "public var \(variableName) = \(dataAsHex.quotedForSourceCode)\n" +let outputString = "public let \(variableName) = \(dataAsHex.quotedForSourceCode)\n" let outputData = outputString.data(using: .utf8) FileManager.default.createFile(atPath: outputFile, contents: outputData) diff --git a/Fixtures/Miscellaneous/Plugins/MySourceGenPlugin/Plugins/MySourceGenPrebuildPlugin/plugin.swift b/Fixtures/Miscellaneous/Plugins/MySourceGenPlugin/Plugins/MySourceGenPrebuildPlugin/plugin.swift index 19dfc360f30..43eff88389f 100644 --- a/Fixtures/Miscellaneous/Plugins/MySourceGenPlugin/Plugins/MySourceGenPrebuildPlugin/plugin.swift +++ b/Fixtures/Miscellaneous/Plugins/MySourceGenPlugin/Plugins/MySourceGenPrebuildPlugin/plugin.swift @@ -1,5 +1,11 @@ import PackagePlugin +#if os(Android) +let touchExe = "/system/bin/touch" +#else +let touchExe = "/usr/bin/touch" +#endif + @main struct MyPlugin: BuildToolPlugin { @@ -15,7 +21,7 @@ struct MyPlugin: BuildToolPlugin { displayName: "Running prebuild command for target \(target.name)", executable: - Path("/usr/bin/touch"), + Path(touchExe), arguments: outputPaths.map{ $0.string }, outputFilesDirectory: diff --git a/Fixtures/Miscellaneous/Plugins/MySourceGenPlugin/Sources/MySourceGenBuildTool/main.swift b/Fixtures/Miscellaneous/Plugins/MySourceGenPlugin/Sources/MySourceGenBuildTool/main.swift index 94f7766628c..bedca50b459 100644 --- a/Fixtures/Miscellaneous/Plugins/MySourceGenPlugin/Sources/MySourceGenBuildTool/main.swift +++ b/Fixtures/Miscellaneous/Plugins/MySourceGenPlugin/Sources/MySourceGenBuildTool/main.swift @@ -13,6 +13,6 @@ let variableName = URL(fileURLWithPath: inputFile).deletingPathExtension().lastP let inputData = FileManager.default.contents(atPath: inputFile) ?? Data() let dataAsHex = inputData.map { String(format: "%02hhx", $0) }.joined() -let outputString = "public var \(variableName) = \(dataAsHex.quotedForSourceCode)\n" +let outputString = "public let \(variableName) = \(dataAsHex.quotedForSourceCode)\n" let outputData = outputString.data(using: .utf8) FileManager.default.createFile(atPath: outputFile, contents: outputData) diff --git a/Fixtures/Miscellaneous/Plugins/MySourceGenPluginNoPreBuildCommands/Package.swift b/Fixtures/Miscellaneous/Plugins/MySourceGenPluginNoPreBuildCommands/Package.swift new file mode 100644 index 00000000000..54f7cead18c --- /dev/null +++ b/Fixtures/Miscellaneous/Plugins/MySourceGenPluginNoPreBuildCommands/Package.swift @@ -0,0 +1,67 @@ +// swift-tools-version: 5.9 +import PackageDescription + +let package = Package( + name: "MySourceGenPluginNoPreBuildCommands", + products: [ + // The product that vends MySourceGenBuildToolPlugin to client packages. + .plugin( + name: "MySourceGenBuildToolPlugin", + targets: ["MySourceGenBuildToolPlugin"] + ), + // The product that vends the MySourceGenBuildTool executable to client packages. + .executable( + name: "MySourceGenBuildTool", + targets: ["MySourceGenBuildTool"] + ), + ], + targets: [ + // A local tool that uses a build tool plugin. + .executableTarget( + name: "MyLocalTool", + plugins: [ + "MySourceGenBuildToolPlugin", + ] + ), + // A local tool that uses a prebuild plugin. + .executableTarget( + name: "MyOtherLocalTool", + plugins: [ + "MySourceGenBuildToolPlugin", + ] + ), + // The plugin that generates build tool commands to invoke MySourceGenBuildTool. + .plugin( + name: "MySourceGenBuildToolPlugin", + capability: .buildTool(), + dependencies: [ + "MySourceGenBuildTool", + ] + ), + // The command line tool that generates source files. + .executableTarget( + name: "MySourceGenBuildTool", + dependencies: [ + "MySourceGenBuildToolLib", + ] + ), + // A library used by MySourceGenBuildTool (not the client). + .target( + name: "MySourceGenBuildToolLib" + ), + // A runtime library that the client needs to link against. + .target( + name: "MySourceGenRuntimeLib" + ), + // Unit tests for the plugin. + .testTarget( + name: "MySourceGenPluginTests", + dependencies: [ + "MySourceGenRuntimeLib", + ], + plugins: [ + "MySourceGenBuildToolPlugin", + ] + ) + ] +) diff --git a/Fixtures/Miscellaneous/Plugins/MySourceGenPluginNoPreBuildCommands/Plugins/MySourceGenBuildToolPlugin/plugin.swift b/Fixtures/Miscellaneous/Plugins/MySourceGenPluginNoPreBuildCommands/Plugins/MySourceGenBuildToolPlugin/plugin.swift new file mode 100644 index 00000000000..5ccedebc704 --- /dev/null +++ b/Fixtures/Miscellaneous/Plugins/MySourceGenPluginNoPreBuildCommands/Plugins/MySourceGenBuildToolPlugin/plugin.swift @@ -0,0 +1,36 @@ +import PackagePlugin + +@main +struct MyPlugin: BuildToolPlugin { + #if USE_CREATE + let verb = "Creating" + #else + let verb = "Generating" + #endif + + func createBuildCommands(context: PluginContext, target: Target) throws -> [Command] { + print("Hello from the Build Tool Plugin!") + guard let target = target as? SourceModuleTarget else { return [] } + return try target.sourceFiles.map{ $0.path }.compactMap { + guard $0.extension == "dat" else { return .none } + let outputName = $0.stem + ".swift" + let outputPath = context.pluginWorkDirectory.appending(outputName) + return .buildCommand( + displayName: + "\(verb) \(outputName) from \($0.lastComponent)", + executable: + try context.tool(named: "MySourceGenBuildTool").path, + arguments: [ + "\($0)", + "\(outputPath)" + ], + inputFiles: [ + $0, + ], + outputFiles: [ + outputPath + ] + ) + } + } +} diff --git a/Fixtures/Miscellaneous/Plugins/MySourceGenPluginNoPreBuildCommands/Sources/MyLocalTool/foo.dat b/Fixtures/Miscellaneous/Plugins/MySourceGenPluginNoPreBuildCommands/Sources/MyLocalTool/foo.dat new file mode 100644 index 00000000000..55cef2c81ff --- /dev/null +++ b/Fixtures/Miscellaneous/Plugins/MySourceGenPluginNoPreBuildCommands/Sources/MyLocalTool/foo.dat @@ -0,0 +1 @@ +I am Foo! diff --git a/Fixtures/Miscellaneous/Plugins/MySourceGenPluginNoPreBuildCommands/Sources/MyLocalTool/main.swift b/Fixtures/Miscellaneous/Plugins/MySourceGenPluginNoPreBuildCommands/Sources/MyLocalTool/main.swift new file mode 100644 index 00000000000..1bbab0f2fc3 --- /dev/null +++ b/Fixtures/Miscellaneous/Plugins/MySourceGenPluginNoPreBuildCommands/Sources/MyLocalTool/main.swift @@ -0,0 +1 @@ +print("Generated string Foo: '\(foo)'") diff --git a/Fixtures/Miscellaneous/Plugins/MySourceGenPluginNoPreBuildCommands/Sources/MyOtherLocalTool/bar.dat b/Fixtures/Miscellaneous/Plugins/MySourceGenPluginNoPreBuildCommands/Sources/MyOtherLocalTool/bar.dat new file mode 100644 index 00000000000..f44d2914ea9 --- /dev/null +++ b/Fixtures/Miscellaneous/Plugins/MySourceGenPluginNoPreBuildCommands/Sources/MyOtherLocalTool/bar.dat @@ -0,0 +1 @@ +I am Bar! diff --git a/Fixtures/Miscellaneous/Plugins/MySourceGenPluginNoPreBuildCommands/Sources/MyOtherLocalTool/baz.dat b/Fixtures/Miscellaneous/Plugins/MySourceGenPluginNoPreBuildCommands/Sources/MyOtherLocalTool/baz.dat new file mode 100644 index 00000000000..e8ebf1dcae7 --- /dev/null +++ b/Fixtures/Miscellaneous/Plugins/MySourceGenPluginNoPreBuildCommands/Sources/MyOtherLocalTool/baz.dat @@ -0,0 +1 @@ +I am Baz! diff --git a/Fixtures/Miscellaneous/Plugins/MySourceGenPluginNoPreBuildCommands/Sources/MyOtherLocalTool/main.swift b/Fixtures/Miscellaneous/Plugins/MySourceGenPluginNoPreBuildCommands/Sources/MyOtherLocalTool/main.swift new file mode 100644 index 00000000000..bdb816d993b --- /dev/null +++ b/Fixtures/Miscellaneous/Plugins/MySourceGenPluginNoPreBuildCommands/Sources/MyOtherLocalTool/main.swift @@ -0,0 +1,2 @@ +// print("Generated string Bar: '\(bar)'") +// print("Generated string Baz: '\(baz)'") diff --git a/Fixtures/Miscellaneous/Plugins/MySourceGenPluginNoPreBuildCommands/Sources/MySourceGenBuildTool/main.swift b/Fixtures/Miscellaneous/Plugins/MySourceGenPluginNoPreBuildCommands/Sources/MySourceGenBuildTool/main.swift new file mode 100644 index 00000000000..94f7766628c --- /dev/null +++ b/Fixtures/Miscellaneous/Plugins/MySourceGenPluginNoPreBuildCommands/Sources/MySourceGenBuildTool/main.swift @@ -0,0 +1,18 @@ +import Foundation +import MySourceGenBuildToolLib + +// Sample source generator tool that emits a Swift variable declaration of a string containing the hex representation of the contents of a file as a quoted string. The variable name is the base name of the input file. The input file is the first argument and the output file is the second. +if ProcessInfo.processInfo.arguments.count != 3 { + print("usage: MySourceGenBuildTool ") + exit(1) +} +let inputFile = ProcessInfo.processInfo.arguments[1] +let outputFile = ProcessInfo.processInfo.arguments[2] + +let variableName = URL(fileURLWithPath: inputFile).deletingPathExtension().lastPathComponent + +let inputData = FileManager.default.contents(atPath: inputFile) ?? Data() +let dataAsHex = inputData.map { String(format: "%02hhx", $0) }.joined() +let outputString = "public var \(variableName) = \(dataAsHex.quotedForSourceCode)\n" +let outputData = outputString.data(using: .utf8) +FileManager.default.createFile(atPath: outputFile, contents: outputData) diff --git a/Fixtures/Miscellaneous/Plugins/MySourceGenPluginNoPreBuildCommands/Sources/MySourceGenBuildToolLib/library.swift b/Fixtures/Miscellaneous/Plugins/MySourceGenPluginNoPreBuildCommands/Sources/MySourceGenBuildToolLib/library.swift new file mode 100644 index 00000000000..25d612434d7 --- /dev/null +++ b/Fixtures/Miscellaneous/Plugins/MySourceGenPluginNoPreBuildCommands/Sources/MySourceGenBuildToolLib/library.swift @@ -0,0 +1,11 @@ +import Foundation + +extension String { + + public var quotedForSourceCode: String { + return "\"" + self + .replacingOccurrences(of: "\\", with: "\\\\") + .replacingOccurrences(of: "\"", with: "\\\"") + + "\"" + } +} diff --git a/Fixtures/Miscellaneous/Plugins/MySourceGenPluginNoPreBuildCommands/Sources/MySourceGenRuntimeLib/library.swift b/Fixtures/Miscellaneous/Plugins/MySourceGenPluginNoPreBuildCommands/Sources/MySourceGenRuntimeLib/library.swift new file mode 100644 index 00000000000..af09355050f --- /dev/null +++ b/Fixtures/Miscellaneous/Plugins/MySourceGenPluginNoPreBuildCommands/Sources/MySourceGenRuntimeLib/library.swift @@ -0,0 +1,3 @@ +public func GetLibraryName() -> String { + return "MySourceGenRuntimeLib" +} diff --git a/Fixtures/Miscellaneous/Plugins/MySourceGenPluginNoPreBuildCommands/Tests/MySourceGenPluginNoPreBuildCommandsTests/MySourceGenPluginNoPreBuildCommandsTests.swift b/Fixtures/Miscellaneous/Plugins/MySourceGenPluginNoPreBuildCommands/Tests/MySourceGenPluginNoPreBuildCommandsTests/MySourceGenPluginNoPreBuildCommandsTests.swift new file mode 100644 index 00000000000..2e44779caee --- /dev/null +++ b/Fixtures/Miscellaneous/Plugins/MySourceGenPluginNoPreBuildCommands/Tests/MySourceGenPluginNoPreBuildCommandsTests/MySourceGenPluginNoPreBuildCommandsTests.swift @@ -0,0 +1,77 @@ +import XCTest +import class Foundation.Bundle + +// for rdar://113256834 +final class SwiftyProtobufTests: XCTestCase { + func testMyLocalTool() throws { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct + // results. + + // Some of the APIs that we use below are available in macOS 10.13 and above. + guard #available(macOS 10.13, *) else { + return + } + + // Mac Catalyst won't have `Process`, but it is supported for executables. + #if !targetEnvironment(macCatalyst) + + let fooBinary = productsDirectory.appendingPathComponent("MyLocalTool") + + let process = Process() + process.executableURL = fooBinary + + let pipe = Pipe() + process.standardOutput = pipe + + try process.run() + process.waitUntilExit() + + let data = pipe.fileHandleForReading.readDataToEndOfFile() + let output = String(data: data, encoding: .utf8) + XCTAssertEqual(output, "Generated string Foo: \'4920616d20466f6f210a\'\n") + #endif + } + + func testMyOtherLocalTool() throws { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct + // results. + + // Some of the APIs that we use below are available in macOS 10.13 and above. + guard #available(macOS 10.13, *) else { + return + } + + // Mac Catalyst won't have `Process`, but it is supported for executables. + #if !targetEnvironment(macCatalyst) + + let fooBinary = productsDirectory.appendingPathComponent("MyOtherLocalTool") + + let process = Process() + process.executableURL = fooBinary + + let pipe = Pipe() + process.standardOutput = pipe + + try process.run() + process.waitUntilExit() + + let data = pipe.fileHandleForReading.readDataToEndOfFile() + let output = String(data: data, encoding: .utf8) + XCTAssertEqual(output, "") + #endif + } + + /// Returns path to the built products directory. + var productsDirectory: URL { + #if os(macOS) + for bundle in Bundle.allBundles where bundle.bundlePath.hasSuffix(".xctest") { + return bundle.bundleURL.deletingLastPathComponent() + } + fatalError("couldn't find the products directory") + #else + return Bundle.main.bundleURL + #endif + } +} diff --git a/Fixtures/Miscellaneous/Plugins/MySourceGenPluginNoPreBuildCommands/Tests/MySourceGenPluginNoPreBuildCommandsTests/lunch.txt b/Fixtures/Miscellaneous/Plugins/MySourceGenPluginNoPreBuildCommands/Tests/MySourceGenPluginNoPreBuildCommandsTests/lunch.txt new file mode 100644 index 00000000000..32e1a738d68 --- /dev/null +++ b/Fixtures/Miscellaneous/Plugins/MySourceGenPluginNoPreBuildCommands/Tests/MySourceGenPluginNoPreBuildCommandsTests/lunch.txt @@ -0,0 +1 @@ +coffee diff --git a/Fixtures/Miscellaneous/Plugins/MySourceGenPluginUsingURLBasedAPI/Package.swift b/Fixtures/Miscellaneous/Plugins/MySourceGenPluginUsingURLBasedAPI/Package.swift index a16a412ff15..35a06c404bc 100644 --- a/Fixtures/Miscellaneous/Plugins/MySourceGenPluginUsingURLBasedAPI/Package.swift +++ b/Fixtures/Miscellaneous/Plugins/MySourceGenPluginUsingURLBasedAPI/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version: 5.11 +// swift-tools-version: 6.0 import PackageDescription let package = Package( diff --git a/Fixtures/Miscellaneous/Plugins/MySourceGenPluginUsingURLBasedAPI/Plugins/MySourceGenPrebuildPlugin/plugin.swift b/Fixtures/Miscellaneous/Plugins/MySourceGenPluginUsingURLBasedAPI/Plugins/MySourceGenPrebuildPlugin/plugin.swift index 0d245d19435..ae7a6aa4e13 100644 --- a/Fixtures/Miscellaneous/Plugins/MySourceGenPluginUsingURLBasedAPI/Plugins/MySourceGenPrebuildPlugin/plugin.swift +++ b/Fixtures/Miscellaneous/Plugins/MySourceGenPluginUsingURLBasedAPI/Plugins/MySourceGenPrebuildPlugin/plugin.swift @@ -1,6 +1,12 @@ import Foundation import PackagePlugin +#if os(Android) +let touchExe = "/system/bin/touch" +#else +let touchExe = "/usr/bin/touch" +#endif + @main struct MyPlugin: BuildToolPlugin { @@ -16,7 +22,7 @@ struct MyPlugin: BuildToolPlugin { displayName: "Running prebuild command for target \(target.name)", executable: - URL(fileURLWithPath: "/usr/bin/touch"), + URL(fileURLWithPath: touchExe), arguments: outputPaths.map{ $0.path }, outputFilesDirectory: diff --git a/Fixtures/Miscellaneous/Plugins/MySourceGenPluginUsingURLBasedAPI/Sources/MySourceGenBuildTool/main.swift b/Fixtures/Miscellaneous/Plugins/MySourceGenPluginUsingURLBasedAPI/Sources/MySourceGenBuildTool/main.swift index 94f7766628c..bedca50b459 100644 --- a/Fixtures/Miscellaneous/Plugins/MySourceGenPluginUsingURLBasedAPI/Sources/MySourceGenBuildTool/main.swift +++ b/Fixtures/Miscellaneous/Plugins/MySourceGenPluginUsingURLBasedAPI/Sources/MySourceGenBuildTool/main.swift @@ -13,6 +13,6 @@ let variableName = URL(fileURLWithPath: inputFile).deletingPathExtension().lastP let inputData = FileManager.default.contents(atPath: inputFile) ?? Data() let dataAsHex = inputData.map { String(format: "%02hhx", $0) }.joined() -let outputString = "public var \(variableName) = \(dataAsHex.quotedForSourceCode)\n" +let outputString = "public let \(variableName) = \(dataAsHex.quotedForSourceCode)\n" let outputData = outputString.data(using: .utf8) FileManager.default.createFile(atPath: outputFile, contents: outputData) diff --git a/Fixtures/Miscellaneous/Plugins/PluginWithInternalExecutable/Sources/PluginExecutable/main.swift b/Fixtures/Miscellaneous/Plugins/PluginWithInternalExecutable/Sources/PluginExecutable/main.swift index 034d9732cd0..6a8b8f23020 100644 --- a/Fixtures/Miscellaneous/Plugins/PluginWithInternalExecutable/Sources/PluginExecutable/main.swift +++ b/Fixtures/Miscellaneous/Plugins/PluginWithInternalExecutable/Sources/PluginExecutable/main.swift @@ -12,6 +12,6 @@ let variableName = URL(fileURLWithPath: inputFile).deletingPathExtension().lastP let inputData = FileManager.default.contents(atPath: inputFile) ?? Data() let dataAsHex = inputData.map { String(format: "%02hhx", $0) }.joined() -let outputString = "public var \(variableName) = \(dataAsHex.quotedForSourceCode)\n" +let outputString = "public let \(variableName) = \(dataAsHex.quotedForSourceCode)\n" let outputData = outputString.data(using: .utf8) FileManager.default.createFile(atPath: outputFile, contents: outputData) diff --git a/Fixtures/Miscellaneous/Plugins/PluginsAndSnippets/Package.swift b/Fixtures/Miscellaneous/Plugins/PluginsAndSnippets/Package.swift index ac5587c1a32..c040212b25d 100644 --- a/Fixtures/Miscellaneous/Plugins/PluginsAndSnippets/Package.swift +++ b/Fixtures/Miscellaneous/Plugins/PluginsAndSnippets/Package.swift @@ -8,7 +8,13 @@ let package = Package( name: "PluginScriptProduct", targets: [ "PluginScriptTarget" - ] + ], + ), + .library( + name: "MyLib", + targets: [ + "MyLib", + ], ), ], targets: [ @@ -17,9 +23,10 @@ let package = Package( capability: .command( intent: .custom( verb: "do-something", - description: "Do something" - ) - ) + description: "Do something", + ), + ), ), + .target(name: "MyLib"), ] ) diff --git a/Fixtures/Miscellaneous/Plugins/PluginsAndSnippets/Plugins/PluginScriptTarget/Script.swift b/Fixtures/Miscellaneous/Plugins/PluginsAndSnippets/Plugins/PluginScriptTarget/Script.swift index 01a0d6c302f..9d835415424 100644 --- a/Fixtures/Miscellaneous/Plugins/PluginsAndSnippets/Plugins/PluginScriptTarget/Script.swift +++ b/Fixtures/Miscellaneous/Plugins/PluginsAndSnippets/Plugins/PluginScriptTarget/Script.swift @@ -4,7 +4,9 @@ import PackagePlugin struct PluginScript: CommandPlugin { func performCommand(context: PluginContext, arguments: [String]) async throws { - dump(context) + if !arguments.contains("--skip-dump") { + dump(context) + } if let target = try context.package.targets(named: ["MySnippet"]).first as? SourceModuleTarget { print("type of snippet target: \(target.kind)") } diff --git a/Fixtures/Miscellaneous/Plugins/PluginsAndSnippets/Snippets/ContainsMain.swift b/Fixtures/Miscellaneous/Plugins/PluginsAndSnippets/Snippets/ContainsMain.swift new file mode 100644 index 00000000000..62a53afc43e --- /dev/null +++ b/Fixtures/Miscellaneous/Plugins/PluginsAndSnippets/Snippets/ContainsMain.swift @@ -0,0 +1,9 @@ +// The Swift Programming Language +// https://docs.swift.org/swift-book + +@main +struct foo { + static func main() { + print("hello, snippets. File: \(#file)") + } +} diff --git a/Fixtures/Miscellaneous/Plugins/PluginsAndSnippets/Snippets/ImportsProductTarget.swift b/Fixtures/Miscellaneous/Plugins/PluginsAndSnippets/Snippets/ImportsProductTarget.swift new file mode 100644 index 00000000000..02b46f32b35 --- /dev/null +++ b/Fixtures/Miscellaneous/Plugins/PluginsAndSnippets/Snippets/ImportsProductTarget.swift @@ -0,0 +1,3 @@ +import MyLib + +libraryCall() \ No newline at end of file diff --git a/Fixtures/Miscellaneous/Plugins/PluginsAndSnippets/Snippets/MySnippet.swift b/Fixtures/Miscellaneous/Plugins/PluginsAndSnippets/Snippets/MySnippet.swift index e69de29bb2d..601b98de34e 100644 --- a/Fixtures/Miscellaneous/Plugins/PluginsAndSnippets/Snippets/MySnippet.swift +++ b/Fixtures/Miscellaneous/Plugins/PluginsAndSnippets/Snippets/MySnippet.swift @@ -0,0 +1 @@ +print("hello, snippets. File: \(#file)") \ No newline at end of file diff --git a/Fixtures/Miscellaneous/Plugins/PluginsAndSnippets/Snippets/SubDirectory/main.swift b/Fixtures/Miscellaneous/Plugins/PluginsAndSnippets/Snippets/SubDirectory/main.swift new file mode 100644 index 00000000000..ab9439d5444 --- /dev/null +++ b/Fixtures/Miscellaneous/Plugins/PluginsAndSnippets/Snippets/SubDirectory/main.swift @@ -0,0 +1 @@ +print("hello, snippets! File: \(#file)") \ No newline at end of file diff --git a/Fixtures/Miscellaneous/Plugins/PluginsAndSnippets/Sources/MyLib/MyLib.swift b/Fixtures/Miscellaneous/Plugins/PluginsAndSnippets/Sources/MyLib/MyLib.swift new file mode 100644 index 00000000000..57aa1a92179 --- /dev/null +++ b/Fixtures/Miscellaneous/Plugins/PluginsAndSnippets/Sources/MyLib/MyLib.swift @@ -0,0 +1,4 @@ +public func libraryCall() { + print("From library") + print("hello, snippets. File: \(#file)") +} \ No newline at end of file diff --git a/Fixtures/Miscellaneous/Plugins/PrebuildDependsExecutableTarget/Package.swift b/Fixtures/Miscellaneous/Plugins/PrebuildDependsExecutableTarget/Package.swift new file mode 100644 index 00000000000..28cc329e122 --- /dev/null +++ b/Fixtures/Miscellaneous/Plugins/PrebuildDependsExecutableTarget/Package.swift @@ -0,0 +1,29 @@ +// swift-tools-version: 6.3 + +import PackageDescription + +let package = Package( + name: "PrebuildDependsExecutableTarget", + platforms: [ .macOS(.v13) ], + dependencies: [ + ], + targets: [ + .executableTarget(name: "MyExecutable"), + + .plugin( + name: "MyPlugin", + capability: .buildTool(), + dependencies: [ + "MyExecutable" + ] + ), + + .executableTarget( + name: "MyClient", + plugins: [ + "MyPlugin", + ] + ), + ], + swiftLanguageModes: [.v5] +) diff --git a/Fixtures/Miscellaneous/Plugins/PrebuildDependsExecutableTarget/Plugins/MyPlugin/plugin.swift b/Fixtures/Miscellaneous/Plugins/PrebuildDependsExecutableTarget/Plugins/MyPlugin/plugin.swift new file mode 100644 index 00000000000..4245e1da853 --- /dev/null +++ b/Fixtures/Miscellaneous/Plugins/PrebuildDependsExecutableTarget/Plugins/MyPlugin/plugin.swift @@ -0,0 +1,22 @@ +import PackagePlugin +import Foundation + +@main +struct MyPlugin: BuildToolPlugin { + func createBuildCommands(context: PluginContext, target: Target) async throws -> [Command] { + let outputFilePath = context.pluginWorkDirectoryURL.appendingPathComponent("MyGeneratedFile.swift") + + // We are attempting to assemble a prebuild command that relies on an executable that hasn't + // been built yet. This should result in an error in prebuild. + let myExecutable = try context.tool(named: "MyExecutable") + + return [ + .prebuildCommand( + displayName: "Prebuild that runs MyExecutable", + executable: myExecutable.url, + arguments: ["--output-file-path", outputFilePath.path], + outputFilesDirectory: context.pluginWorkDirectoryURL + ) + ] + } +} diff --git a/Fixtures/Miscellaneous/Plugins/PrebuildDependsExecutableTarget/Sources/MyClient/client.swift b/Fixtures/Miscellaneous/Plugins/PrebuildDependsExecutableTarget/Sources/MyClient/client.swift new file mode 100644 index 00000000000..2b522f38b32 --- /dev/null +++ b/Fixtures/Miscellaneous/Plugins/PrebuildDependsExecutableTarget/Sources/MyClient/client.swift @@ -0,0 +1,6 @@ +@main +struct Client { + static func main() throws { + print(MyGeneratedStruct.message) + } +} diff --git a/Fixtures/Miscellaneous/Plugins/PrebuildDependsExecutableTarget/Sources/MyExecutable/tool.swift b/Fixtures/Miscellaneous/Plugins/PrebuildDependsExecutableTarget/Sources/MyExecutable/tool.swift new file mode 100644 index 00000000000..d319ed9bd67 --- /dev/null +++ b/Fixtures/Miscellaneous/Plugins/PrebuildDependsExecutableTarget/Sources/MyExecutable/tool.swift @@ -0,0 +1,26 @@ +import Foundation + +@main +struct Tool { + static func main() async throws { + print("warning: Whoops! Coming from the executable", to: &StdErr.shared) + + let path = CommandLine.arguments[2] + print("Writing a file to \(path)") + + try #""" + public struct MyGeneratedStruct { + public static var message: String = "You got struct'd" + } + """#.write(to: URL(fileURLWithPath: path), atomically: true, encoding: .utf8) + } +} + +struct StdErr: TextOutputStream { + static var shared: Self = .init() + mutating func write(_ string: String) { + string.withCString { ptr in + _ = fputs(ptr, stderr) + } + } +} diff --git a/Fixtures/Miscellaneous/Plugins/SwiftFilePlugin/Package.swift b/Fixtures/Miscellaneous/Plugins/SwiftFilePlugin/Package.swift new file mode 100644 index 00000000000..8f1d2d37eab --- /dev/null +++ b/Fixtures/Miscellaneous/Plugins/SwiftFilePlugin/Package.swift @@ -0,0 +1,25 @@ +// swift-tools-version: 6.0 + +import PackageDescription + +let package = Package( + name: "SwiftFilePluginFixture", + products: [ + .library( + name: "SwiftFilePluginFixture", + targets: ["SwiftFilePluginFixture"] + ), + ], + targets: [ + .target( + name: "SwiftFilePluginFixture", + plugins: [ + .plugin(name: "MyCustomBuildTool") + ] + ), + .plugin( + name: "MyCustomBuildTool", + capability: .buildTool() + ) + ] +) diff --git a/Fixtures/Miscellaneous/Plugins/SwiftFilePlugin/Plugins/MyCustomBuildTool/SwiftToolsBuildPlugin.swift b/Fixtures/Miscellaneous/Plugins/SwiftFilePlugin/Plugins/MyCustomBuildTool/SwiftToolsBuildPlugin.swift new file mode 100644 index 00000000000..d73c8588a9b --- /dev/null +++ b/Fixtures/Miscellaneous/Plugins/SwiftFilePlugin/Plugins/MyCustomBuildTool/SwiftToolsBuildPlugin.swift @@ -0,0 +1,16 @@ +import Foundation +import PackagePlugin + +@main +struct SwiftToolsBuildPlugin: BuildToolPlugin { + func createBuildCommands(context: PluginContext, target: Target) async throws -> [Command] { + let formatExecutable = context.package.directoryURL.appending(components: "SimpleSwiftScript.swift") + return [.buildCommand( + displayName: "Run a swift script", + executable: formatExecutable, + arguments: [], + inputFiles: [formatExecutable], + outputFiles: [] + )] + } +} diff --git a/Fixtures/Miscellaneous/Plugins/SwiftFilePlugin/SimpleSwiftScript.swift b/Fixtures/Miscellaneous/Plugins/SwiftFilePlugin/SimpleSwiftScript.swift new file mode 100755 index 00000000000..e96619a9846 --- /dev/null +++ b/Fixtures/Miscellaneous/Plugins/SwiftFilePlugin/SimpleSwiftScript.swift @@ -0,0 +1,3 @@ +#!/usr/bin/env swift + +print("Hello, Build Tool Plugin!") diff --git a/Fixtures/Miscellaneous/Plugins/SwiftFilePlugin/Sources/SwiftFilePluginFixture/SwiftFilePluginFixture.swift b/Fixtures/Miscellaneous/Plugins/SwiftFilePlugin/Sources/SwiftFilePluginFixture/SwiftFilePluginFixture.swift new file mode 100644 index 00000000000..e69de29bb2d diff --git a/Fixtures/Miscellaneous/RequiresOlderDeploymentTarget/Foo.swift b/Fixtures/Miscellaneous/RequiresOlderDeploymentTarget/Foo.swift new file mode 100644 index 00000000000..aeeb2c29b21 --- /dev/null +++ b/Fixtures/Miscellaneous/RequiresOlderDeploymentTarget/Foo.swift @@ -0,0 +1,8 @@ +@available(macOS, obsoleted: 13.0) +func foo() { + +} + +func bar() { + foo() +} diff --git a/Fixtures/Miscellaneous/RequiresOlderDeploymentTarget/Package.swift b/Fixtures/Miscellaneous/RequiresOlderDeploymentTarget/Package.swift new file mode 100644 index 00000000000..220ca90d3a9 --- /dev/null +++ b/Fixtures/Miscellaneous/RequiresOlderDeploymentTarget/Package.swift @@ -0,0 +1,13 @@ +// swift-tools-version:6.1 +import PackageDescription + +let package = Package( + name: "Foo", + platforms: [.macOS(.v12)], + products: [ + .library(name: "Foo", targets: ["Foo"]), + ], + targets: [ + .target(name: "Foo", path: "./"), + ] +) diff --git a/Fixtures/Miscellaneous/ShowExecutables/app/Package.swift b/Fixtures/Miscellaneous/ShowExecutables/app/Package.swift new file mode 100644 index 00000000000..389ab1be39c --- /dev/null +++ b/Fixtures/Miscellaneous/ShowExecutables/app/Package.swift @@ -0,0 +1,21 @@ +// swift-tools-version:5.10 +import PackageDescription + +let package = Package( + name: "Dealer", + products: [ + .executable( + name: "dealer", + targets: ["Dealer"] + ), + ], + dependencies: [ + .package(path: "../deck-of-playing-cards"), + ], + targets: [ + .executableTarget( + name: "Dealer", + path: "./" + ), + ] +) diff --git a/Fixtures/Miscellaneous/ShowExecutables/app/main.swift b/Fixtures/Miscellaneous/ShowExecutables/app/main.swift new file mode 100644 index 00000000000..83174e0afff --- /dev/null +++ b/Fixtures/Miscellaneous/ShowExecutables/app/main.swift @@ -0,0 +1 @@ +print("I'm the dealer") diff --git a/Fixtures/Miscellaneous/ShowExecutables/deck-of-playing-cards/Package.swift b/Fixtures/Miscellaneous/ShowExecutables/deck-of-playing-cards/Package.swift new file mode 100644 index 00000000000..0c23f679535 --- /dev/null +++ b/Fixtures/Miscellaneous/ShowExecutables/deck-of-playing-cards/Package.swift @@ -0,0 +1,18 @@ +// swift-tools-version:5.10 +import PackageDescription + +let package = Package( + name: "deck-of-playing-cards", + products: [ + .executable( + name: "deck", + targets: ["Deck"] + ), + ], + targets: [ + .executableTarget( + name: "Deck", + path: "./" + ), + ] +) diff --git a/Fixtures/Miscellaneous/ShowExecutables/deck-of-playing-cards/main.swift b/Fixtures/Miscellaneous/ShowExecutables/deck-of-playing-cards/main.swift new file mode 100644 index 00000000000..51bd0e13de2 --- /dev/null +++ b/Fixtures/Miscellaneous/ShowExecutables/deck-of-playing-cards/main.swift @@ -0,0 +1 @@ +print("I'm a deck of cards") 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/Fixtures/Miscellaneous/SwiftBuild/Package.swift b/Fixtures/Miscellaneous/SwiftBuild/Package.swift new file mode 100644 index 00000000000..ace9f61f0cc --- /dev/null +++ b/Fixtures/Miscellaneous/SwiftBuild/Package.swift @@ -0,0 +1,12 @@ +// swift-tools-version: 5.6 +import PackageDescription + +let package = Package( + name: "TestableExe", + targets: [ + .executableTarget( + name: "Test", + path: "." + ), + ] +) diff --git a/Fixtures/Miscellaneous/SwiftBuild/main.swift b/Fixtures/Miscellaneous/SwiftBuild/main.swift new file mode 100644 index 00000000000..621980f7b8b --- /dev/null +++ b/Fixtures/Miscellaneous/SwiftBuild/main.swift @@ -0,0 +1 @@ +print("done") \ No newline at end of file diff --git a/Fixtures/Miscellaneous/TestDiscovery/IgnoresLinuxMain/Package.swift b/Fixtures/Miscellaneous/TestDiscovery/IgnoresLinuxMain/Package.swift new file mode 100644 index 00000000000..d7749959468 --- /dev/null +++ b/Fixtures/Miscellaneous/TestDiscovery/IgnoresLinuxMain/Package.swift @@ -0,0 +1,9 @@ +// swift-tools-version:5.10 +import PackageDescription + +let package = Package( + name: "IgnoresLinuxMain", + targets: [ + .testTarget(name: "IgnoresLinuxMainTests"), + ] +) diff --git a/Fixtures/Miscellaneous/TestDiscovery/IgnoresLinuxMain/Tests/IgnoresLinuxMainTests/SomeTest.swift b/Fixtures/Miscellaneous/TestDiscovery/IgnoresLinuxMain/Tests/IgnoresLinuxMainTests/SomeTest.swift new file mode 100644 index 00000000000..dec36ed294c --- /dev/null +++ b/Fixtures/Miscellaneous/TestDiscovery/IgnoresLinuxMain/Tests/IgnoresLinuxMainTests/SomeTest.swift @@ -0,0 +1,5 @@ +import XCTest + +final class SomeTests: XCTestCase { + func testSomething() {} +} diff --git a/Fixtures/Miscellaneous/TestDiscovery/IgnoresLinuxMain/Tests/LinuxMain.swift b/Fixtures/Miscellaneous/TestDiscovery/IgnoresLinuxMain/Tests/LinuxMain.swift new file mode 100644 index 00000000000..f7b651fc7aa --- /dev/null +++ b/Fixtures/Miscellaneous/TestDiscovery/IgnoresLinuxMain/Tests/LinuxMain.swift @@ -0,0 +1 @@ +fatalError("Should not use the contents of LinuxMain.swift") diff --git a/Fixtures/Miscellaneous/TestDiscovery/SwiftTesting/Package.swift b/Fixtures/Miscellaneous/TestDiscovery/SwiftTesting/Package.swift index 2588f440a55..927603bf003 100644 --- a/Fixtures/Miscellaneous/TestDiscovery/SwiftTesting/Package.swift +++ b/Fixtures/Miscellaneous/TestDiscovery/SwiftTesting/Package.swift @@ -1,18 +1,9 @@ -// swift-tools-version: 5.10 +// swift-tools-version: 6.0 import PackageDescription let package = Package( name: "SwiftTesting", - platforms: [ - .macOS(.v13), .iOS(.v16), .watchOS(.v9), .tvOS(.v16), .visionOS(.v1) - ], - dependencies: [ - .package(url: "https://github.com/apple/swift-testing.git", branch: "main"), - ], targets: [ - .testTarget( - name: "SwiftTestingTests", - dependencies: [.product(name: "Testing", package: "swift-testing"),] - ), + .testTarget(name: "SwiftTestingTests"), ] ) diff --git a/Fixtures/Miscellaneous/TestMultipleFailureSwiftTesting/Package.swift b/Fixtures/Miscellaneous/TestMultipleFailureSwiftTesting/Package.swift new file mode 100644 index 00000000000..554233406da --- /dev/null +++ b/Fixtures/Miscellaneous/TestMultipleFailureSwiftTesting/Package.swift @@ -0,0 +1,13 @@ +// swift-tools-version: 6.0 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "TestMultipleFailureSwiftTesting", + targets: [ + .testTarget( + name: "TestMultipleFailureSwiftTestingTests" + ) + ] +) diff --git a/Fixtures/Miscellaneous/TestMultipleFailureSwiftTesting/Tests/TestMultipleFailureSwiftTestingTests/TestMultipleFailureSwiftTestingTests.swift b/Fixtures/Miscellaneous/TestMultipleFailureSwiftTesting/Tests/TestMultipleFailureSwiftTestingTests/TestMultipleFailureSwiftTestingTests.swift new file mode 100644 index 00000000000..bb15cc80df0 --- /dev/null +++ b/Fixtures/Miscellaneous/TestMultipleFailureSwiftTesting/Tests/TestMultipleFailureSwiftTestingTests/TestMultipleFailureSwiftTestingTests.swift @@ -0,0 +1,41 @@ +import Testing + +@Test func testFailure1() async throws { + #expect(Bool(false), "ST Test failure 1") +} + +@Test func testFailure2() async throws { + #expect(Bool(false), "ST Test failure 2") +} + +@Test func testFailure3() async throws { + #expect(Bool(false), "ST Test failure 3") +} + +@Test func testFailure4() async throws { + #expect(Bool(false), "ST Test failure 4") +} + +@Test func testFailure5() async throws { + #expect(Bool(false), "ST Test failure 5") +} + +@Test func testFailure6() async throws { + #expect(Bool(false), "ST Test failure 6") +} + +@Test func testFailure7() async throws { + #expect(Bool(false), "ST Test failure 7") +} + +@Test func testFailure8() async throws { + #expect(Bool(false), "ST Test failure 8") +} + +@Test func testFailure9() async throws { + #expect(Bool(false), "ST Test failure 9") +} + +@Test func testFailure10() async throws { + #expect(Bool(false), "ST Test failure 10") +} diff --git a/Fixtures/Miscellaneous/TestMultipleFailureXCTest/Package.swift b/Fixtures/Miscellaneous/TestMultipleFailureXCTest/Package.swift new file mode 100644 index 00000000000..ba5fece9a8a --- /dev/null +++ b/Fixtures/Miscellaneous/TestMultipleFailureXCTest/Package.swift @@ -0,0 +1,13 @@ +// swift-tools-version: 5.9 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "TestMultipleFailureXCTest", + targets: [ + .testTarget( + name: "TestMultipleFailureXCTestTests" + ) + ] +) diff --git a/Fixtures/Miscellaneous/TestMultipleFailureXCTest/Tests/TestMultipleFailureXCTestTests/TestMultipleFailureXCTestTests.swift b/Fixtures/Miscellaneous/TestMultipleFailureXCTest/Tests/TestMultipleFailureXCTestTests/TestMultipleFailureXCTestTests.swift new file mode 100644 index 00000000000..57a1d8068bf --- /dev/null +++ b/Fixtures/Miscellaneous/TestMultipleFailureXCTest/Tests/TestMultipleFailureXCTestTests/TestMultipleFailureXCTestTests.swift @@ -0,0 +1,44 @@ +import XCTest + +final class TestMultipleFailureXCTestTests: XCTestCase { + func testFailure1() throws { + XCTAssertFalse(true, "Test failure 1") + } + + func testFailure2() throws { + XCTAssertFalse(true, "Test failure 2") + } + + func testFailure3() throws { + XCTAssertFalse(true, "Test failure 3") + } + + func testFailure4() throws { + XCTAssertFalse(true, "Test failure 4") + } + + func testFailure5() throws { + XCTAssertFalse(true, "Test failure 5") + } + + func testFailure6() throws { + XCTAssertFalse(true, "Test failure 6") + } + + func testFailure7() throws { + XCTAssertFalse(true, "Test failure 7") + } + + func testFailure8() throws { + XCTAssertFalse(true, "Test failure 8") + } + + func testFailure9() throws { + XCTAssertFalse(true, "Test failure 9") + } + + func testFailure10() throws { + XCTAssertFalse(true, "Test failure 10") + } + +} diff --git a/Fixtures/Miscellaneous/TestSingleFailureSwiftTesting/Package.swift b/Fixtures/Miscellaneous/TestSingleFailureSwiftTesting/Package.swift new file mode 100644 index 00000000000..5204e069abb --- /dev/null +++ b/Fixtures/Miscellaneous/TestSingleFailureSwiftTesting/Package.swift @@ -0,0 +1,13 @@ +// swift-tools-version: 5.9 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "TestFailuresSwiftTesting", + targets: [ + .testTarget( + name: "TestFailuresSwiftTestingTests" + ) + ] +) diff --git a/Fixtures/Miscellaneous/TestSingleFailureSwiftTesting/Tests/TestFailuresSwiftTestingTests/TestFailuresSwiftTestingTests.swift b/Fixtures/Miscellaneous/TestSingleFailureSwiftTesting/Tests/TestFailuresSwiftTestingTests/TestFailuresSwiftTestingTests.swift new file mode 100644 index 00000000000..896c1e8ca07 --- /dev/null +++ b/Fixtures/Miscellaneous/TestSingleFailureSwiftTesting/Tests/TestFailuresSwiftTestingTests/TestFailuresSwiftTestingTests.swift @@ -0,0 +1,5 @@ +import Testing + +@Test func example() async throws { + #expect(Bool(false), "Purposely failing & validating XML espace \"'<>") +} diff --git a/Fixtures/Miscellaneous/TestSingleFailureXCTest/Package.swift b/Fixtures/Miscellaneous/TestSingleFailureXCTest/Package.swift new file mode 100644 index 00000000000..445ff2a13a6 --- /dev/null +++ b/Fixtures/Miscellaneous/TestSingleFailureXCTest/Package.swift @@ -0,0 +1,13 @@ +// swift-tools-version: 5.9 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "TestFailures", + targets: [ + .testTarget( + name: "TestFailuresTests" + ) + ] +) diff --git a/Fixtures/Miscellaneous/TestSingleFailureXCTest/Tests/TestFailuresTests/TestFailuresTests.swift b/Fixtures/Miscellaneous/TestSingleFailureXCTest/Tests/TestFailuresTests/TestFailuresTests.swift new file mode 100644 index 00000000000..bb507ae4e1e --- /dev/null +++ b/Fixtures/Miscellaneous/TestSingleFailureXCTest/Tests/TestFailuresTests/TestFailuresTests.swift @@ -0,0 +1,8 @@ +import XCTest + +final class TestFailuresTests: XCTestCase { + func testExample() throws { + XCTAssertFalse(true, "Purposely failing & validating XML espace \"'<>") + } +} + diff --git a/Fixtures/Miscellaneous/TestableAsyncExe/Package.swift b/Fixtures/Miscellaneous/TestableAsyncExe/Package.swift index 289632dccda..557b815eb53 100644 --- a/Fixtures/Miscellaneous/TestableAsyncExe/Package.swift +++ b/Fixtures/Miscellaneous/TestableAsyncExe/Package.swift @@ -3,6 +3,9 @@ import PackageDescription let package = Package( name: "TestableAsyncExe", + platforms: [ + .macOS(.v10_15), + ], targets: [ .executableTarget( name: "TestableAsyncExe1" diff --git a/Fixtures/Miscellaneous/TestableExe/Package.swift b/Fixtures/Miscellaneous/TestableExe/Package.swift index 1de311d1c13..6546a0b9a81 100644 --- a/Fixtures/Miscellaneous/TestableExe/Package.swift +++ b/Fixtures/Miscellaneous/TestableExe/Package.swift @@ -5,13 +5,23 @@ let package = Package( name: "TestableExe", targets: [ .executableTarget( - name: "TestableExe1" + name: "TestableExe1", + linkerSettings: [ + .linkedLibrary("swiftCore", .when(platforms: [.windows])), // for swift_addNewDSOImage + ] ), .executableTarget( - name: "TestableExe2" + name: "TestableExe2", + linkerSettings: [ + .linkedLibrary("swiftCore", .when(platforms: [.windows])), // for swift_addNewDSOImage + ] + ), .executableTarget( - name: "TestableExe3" + name: "TestableExe3", + linkerSettings: [ + .linkedLibrary("swiftCore", .when(platforms: [.windows])), // for swift_addNewDSOImage + ] ), .testTarget( name: "TestableExeTests", diff --git a/Fixtures/Miscellaneous/TestableExe/Tests/TestableExeTests/TestableExeTests.swift b/Fixtures/Miscellaneous/TestableExe/Tests/TestableExeTests/TestableExeTests.swift index 33b94c96102..75a8cee57f0 100644 --- a/Fixtures/Miscellaneous/TestableExe/Tests/TestableExeTests/TestableExeTests.swift +++ b/Fixtures/Miscellaneous/TestableExe/Tests/TestableExeTests/TestableExeTests.swift @@ -5,6 +5,13 @@ import XCTest import class Foundation.Bundle final class TestableExeTests: XCTestCase { + + #if os(Windows) + let eol = "\r\n" +#else + let eol = "\n" +#endif + func testExample() throws { // This is an example of a functional test case. // Use XCTAssert and related functions to verify your tests produce the correct @@ -30,7 +37,7 @@ final class TestableExeTests: XCTestCase { process.waitUntilExit() var data = pipe.fileHandleForReading.readDataToEndOfFile() var output = String(data: data, encoding: .utf8) - XCTAssertEqual(output, "Hello, world!\n") + XCTAssertEqual(output, "Hello, world!\(eol)") execPath = productsDirectory.appendingPathComponent("TestableExe2") process = Process() @@ -41,7 +48,7 @@ final class TestableExeTests: XCTestCase { process.waitUntilExit() data = pipe.fileHandleForReading.readDataToEndOfFile() output = String(data: data, encoding: .utf8) - XCTAssertEqual(output, "Hello, planet!\n") + XCTAssertEqual(output, "Hello, planet!\(eol)") execPath = productsDirectory.appendingPathComponent("TestableExe3") process = Process() @@ -52,7 +59,7 @@ final class TestableExeTests: XCTestCase { process.waitUntilExit() data = pipe.fileHandleForReading.readDataToEndOfFile() output = String(data: data, encoding: .utf8) - XCTAssertEqual(output, "Hello, universe!\n") + XCTAssertEqual(output, "Hello, universe!\(eol)") } /// Returns path to the built products directory. diff --git a/Fixtures/Miscellaneous/Unicode/Utilities/SomeOtherPackage/README.md b/Fixtures/Miscellaneous/Unicode/Utilities/SomeOtherPackage/README.md index 47ee364eaa7..84138780751 100644 --- a/Fixtures/Miscellaneous/Unicode/Utilities/SomeOtherPackage/README.md +++ b/Fixtures/Miscellaneous/Unicode/Utilities/SomeOtherPackage/README.md @@ -2,6 +2,6 @@ This nested utility package refers back to the main repository as a local dependency. -If client tools accidently pick this package up as part of the main repository package, dependency resolution could become circular or otherwise problematic. +If client tools accidentally pick this package up as part of the main repository package, dependency resolution could become circular or otherwise problematic. (Prior to direct Xcode support for packages, this repository structure was a common way of hiding executable utilities from `generate-xcodeproj`, so that the main package would still be viable for iOS.) diff --git a/Fixtures/ModuleAliasing/DirectDeps2/AppPkg/Package.swift b/Fixtures/ModuleAliasing/DirectDeps2/AppPkg/Package.swift index ea170d60f2a..63294ac0ee7 100644 --- a/Fixtures/ModuleAliasing/DirectDeps2/AppPkg/Package.swift +++ b/Fixtures/ModuleAliasing/DirectDeps2/AppPkg/Package.swift @@ -19,7 +19,8 @@ let package = Package( package: "Bpkg", moduleAliases: ["Utils": "BUtils"] ) - ]), + ] + ), ] ) diff --git a/Fixtures/ModuleAliasing/NestedDeps1/AppPkg/Sources/App/main.swift b/Fixtures/ModuleAliasing/NestedDeps1/AppPkg/Sources/App/main.swift index aebfa467f3b..1092081d3e2 100644 --- a/Fixtures/ModuleAliasing/NestedDeps1/AppPkg/Sources/App/main.swift +++ b/Fixtures/ModuleAliasing/NestedDeps1/AppPkg/Sources/App/main.swift @@ -3,7 +3,7 @@ import X import AFooUtils import CarUtils import XFooUtils -import Utils +import XUtils funcInA() funcInX() diff --git a/Fixtures/ModuleAliasing/NestedDeps2/AppPkg/Sources/App/main.swift b/Fixtures/ModuleAliasing/NestedDeps2/AppPkg/Sources/App/main.swift index 203b02d4777..529dfdded7c 100644 --- a/Fixtures/ModuleAliasing/NestedDeps2/AppPkg/Sources/App/main.swift +++ b/Fixtures/ModuleAliasing/NestedDeps2/AppPkg/Sources/App/main.swift @@ -9,7 +9,7 @@ */ import A -import Utils +import XUtils print("START") diff --git a/Fixtures/PIFBuilder/CCPackage/Package.swift b/Fixtures/PIFBuilder/CCPackage/Package.swift new file mode 100644 index 00000000000..0237d0bd0f4 --- /dev/null +++ b/Fixtures/PIFBuilder/CCPackage/Package.swift @@ -0,0 +1,14 @@ +// swift-tools-version: 6.2 + +import PackageDescription + +let package = Package( + name: "CCPackage", + products: [ + .library(name: "CCTarget", type: .static, targets: ["CCTarget"]), + ], + targets: [ + .target(name: "CCTarget", ), + .executableTarget(name: "executable", dependencies: ["CCTarget"]), + ] +) diff --git a/Fixtures/PIFBuilder/CCPackage/Sources/CCTarget/include/test.h b/Fixtures/PIFBuilder/CCPackage/Sources/CCTarget/include/test.h new file mode 100644 index 00000000000..4d9ece60cb0 --- /dev/null +++ b/Fixtures/PIFBuilder/CCPackage/Sources/CCTarget/include/test.h @@ -0,0 +1,2 @@ + +#include \ No newline at end of file diff --git a/Fixtures/PIFBuilder/CCPackage/Sources/CCTarget/test.cc b/Fixtures/PIFBuilder/CCPackage/Sources/CCTarget/test.cc new file mode 100644 index 00000000000..dd8f2c6c542 --- /dev/null +++ b/Fixtures/PIFBuilder/CCPackage/Sources/CCTarget/test.cc @@ -0,0 +1,2 @@ + +#include diff --git a/Fixtures/PIFBuilder/CCPackage/Sources/executable/executable.swift b/Fixtures/PIFBuilder/CCPackage/Sources/executable/executable.swift new file mode 100644 index 00000000000..4190ba6a221 --- /dev/null +++ b/Fixtures/PIFBuilder/CCPackage/Sources/executable/executable.swift @@ -0,0 +1,9 @@ +// The Swift Programming Language +// https://docs.swift.org/swift-book + +@main +struct Executable { + static func main() { + print("Hello, world!") + } +} diff --git a/Fixtures/PIFBuilder/UnknownPlatforms/Package.swift b/Fixtures/PIFBuilder/UnknownPlatforms/Package.swift new file mode 100644 index 00000000000..6f791248e21 --- /dev/null +++ b/Fixtures/PIFBuilder/UnknownPlatforms/Package.swift @@ -0,0 +1,17 @@ +// swift-tools-version: 6.2 + +import PackageDescription + +let package = Package( + name: "UnknownPlatforms", + targets: [ + .executableTarget( + name: "UnknownPlatforms", + swiftSettings: [ + .define("FOO", .when(platforms: [.custom("DoesNotExist")])), + .define("BAR", .when(platforms: [.linux])), + .define("BAZ", .when(platforms: [.macOS])), + ], + ), + ] +) diff --git a/Fixtures/PIFBuilder/UnknownPlatforms/Sources/UnknownPlatforms/UnknownPlatforms.swift b/Fixtures/PIFBuilder/UnknownPlatforms/Sources/UnknownPlatforms/UnknownPlatforms.swift new file mode 100644 index 00000000000..7ab2b4f88fa --- /dev/null +++ b/Fixtures/PIFBuilder/UnknownPlatforms/Sources/UnknownPlatforms/UnknownPlatforms.swift @@ -0,0 +1,9 @@ +// The Swift Programming Language +// https://docs.swift.org/swift-book + +@main +struct UnknownPlatforms { + static func main() { + print("Hello, world!") + } +} diff --git a/Fixtures/PartiallyUnusedDependency/Dep/Package.swift b/Fixtures/PartiallyUnusedDependency/Dep/Package.swift new file mode 100644 index 00000000000..4046c584594 --- /dev/null +++ b/Fixtures/PartiallyUnusedDependency/Dep/Package.swift @@ -0,0 +1,27 @@ +// swift-tools-version: 6.0 + +import PackageDescription + +let package = Package( + name: "Dep", + products: [ + .library( + name: "MyDynamicLibrary", + type: .dynamic, + targets: ["MyDynamicLibrary"] + ), + .executable( + name: "MySupportExecutable", + targets: ["MySupportExecutable"] + ) + ], + targets: [ + .target( + name: "MyDynamicLibrary" + ), + .executableTarget( + name: "MySupportExecutable", + dependencies: ["MyDynamicLibrary"] + ) + ] +) diff --git a/Fixtures/PartiallyUnusedDependency/Dep/Sources/MyDynamicLibrary/Dep.swift b/Fixtures/PartiallyUnusedDependency/Dep/Sources/MyDynamicLibrary/Dep.swift new file mode 100644 index 00000000000..760f5831aad --- /dev/null +++ b/Fixtures/PartiallyUnusedDependency/Dep/Sources/MyDynamicLibrary/Dep.swift @@ -0,0 +1,3 @@ +public func sayHello() { + print("hello!") +} diff --git a/Fixtures/PartiallyUnusedDependency/Dep/Sources/MySupportExecutable/exe.swift b/Fixtures/PartiallyUnusedDependency/Dep/Sources/MySupportExecutable/exe.swift new file mode 100644 index 00000000000..063d151101c --- /dev/null +++ b/Fixtures/PartiallyUnusedDependency/Dep/Sources/MySupportExecutable/exe.swift @@ -0,0 +1,8 @@ +import MyDynamicLibrary + +@main struct Entry { + static func main() { + print("running support tool") + sayHello() + } +} diff --git a/Fixtures/PartiallyUnusedDependency/Package.swift b/Fixtures/PartiallyUnusedDependency/Package.swift new file mode 100644 index 00000000000..40d5345f5db --- /dev/null +++ b/Fixtures/PartiallyUnusedDependency/Package.swift @@ -0,0 +1,29 @@ +// swift-tools-version: 6.0 + +import PackageDescription + +let package = Package( + name: "PartiallyUnusedDependency", + products: [ + .executable( + name: "MyExecutable", + targets: ["MyExecutable"] + ), + ], + dependencies: [ + .package(path: "Dep") + ], + targets: [ + .executableTarget( + name: "MyExecutable", + dependencies: [.product(name: "MyDynamicLibrary", package: "Dep")] + ), + .plugin( + name: "dump-artifacts-plugin", + capability: .command( + intent: .custom(verb: "dump-artifacts-plugin", description: "Dump Artifacts"), + permissions: [] + ) + ) + ] +) diff --git a/Fixtures/PartiallyUnusedDependency/Plugins/dump-artifacts-plugin.swift b/Fixtures/PartiallyUnusedDependency/Plugins/dump-artifacts-plugin.swift new file mode 100644 index 00000000000..cebbb779ee3 --- /dev/null +++ b/Fixtures/PartiallyUnusedDependency/Plugins/dump-artifacts-plugin.swift @@ -0,0 +1,24 @@ +import PackagePlugin + +@main +struct DumpArtifactsPlugin: CommandPlugin { + func performCommand( + context: PluginContext, + arguments: [String] + ) throws { + do { + var parameters = PackageManager.BuildParameters() + parameters.configuration = .debug + parameters.logging = .concise + let result = try packageManager.build(.all(includingTests: false), parameters: parameters) + print("succeeded: \(result.succeeded)") + for artifact in result.builtArtifacts { + print("artifact-path: \(artifact.path.string)") + print("artifact-kind: \(artifact.kind)") + } + } + catch { + print("error from the plugin host: \\(error)") + } + } +} diff --git a/Fixtures/PartiallyUnusedDependency/Sources/MyExecutable/PartiallyUnusedDependency.swift b/Fixtures/PartiallyUnusedDependency/Sources/MyExecutable/PartiallyUnusedDependency.swift new file mode 100644 index 00000000000..daa68135bb4 --- /dev/null +++ b/Fixtures/PartiallyUnusedDependency/Sources/MyExecutable/PartiallyUnusedDependency.swift @@ -0,0 +1,8 @@ +import MyDynamicLibrary + +@main struct Entry { + static func main() { + print("Hello, world!") + sayHello() + } +} diff --git a/Fixtures/Resources/Simple/Sources/MixedClangResource/baz.S b/Fixtures/Resources/Simple/Sources/MixedClangResource/baz.S new file mode 100644 index 00000000000..e69de29bb2d diff --git a/Fixtures/Resources/Simple/Sources/MixedClangResource/qux.cpp b/Fixtures/Resources/Simple/Sources/MixedClangResource/qux.cpp new file mode 100644 index 00000000000..e69de29bb2d diff --git a/Fixtures/SwiftMigrate/ExistentialAnyMigration/Package.swift b/Fixtures/SwiftMigrate/ExistentialAnyMigration/Package.swift new file mode 100644 index 00000000000..a85e03ae79d --- /dev/null +++ b/Fixtures/SwiftMigrate/ExistentialAnyMigration/Package.swift @@ -0,0 +1,10 @@ +// swift-tools-version:5.8 + +import PackageDescription + +let package = Package( + name: "ExistentialAnyMigration", + targets: [ + .target(name: "Diagnostics", path: "Sources", exclude: ["Fixed"]), + ] +) diff --git a/Fixtures/SwiftMigrate/ExistentialAnyMigration/Sources/Fixed/Test.swift b/Fixtures/SwiftMigrate/ExistentialAnyMigration/Sources/Fixed/Test.swift new file mode 100644 index 00000000000..1b6fd4581b9 --- /dev/null +++ b/Fixtures/SwiftMigrate/ExistentialAnyMigration/Sources/Fixed/Test.swift @@ -0,0 +1,21 @@ +protocol P { +} +protocol Q { +} + +func test1(_: any P) { +} + +func test2(_: (any P).Protocol) { +} + +func test3() { + let _: [(any P)?] = [] +} + +func test4() { + var x = 42 +} + +func test5(_: any P & Q) { +} diff --git a/Fixtures/SwiftMigrate/ExistentialAnyMigration/Sources/Test.swift b/Fixtures/SwiftMigrate/ExistentialAnyMigration/Sources/Test.swift new file mode 100644 index 00000000000..abfc872e2b4 --- /dev/null +++ b/Fixtures/SwiftMigrate/ExistentialAnyMigration/Sources/Test.swift @@ -0,0 +1,21 @@ +protocol P { +} +protocol Q { +} + +func test1(_: P) { +} + +func test2(_: P.Protocol) { +} + +func test3() { + let _: [P?] = [] +} + +func test4() { + var x = 42 +} + +func test5(_: P & Q) { +} diff --git a/Fixtures/SwiftMigrate/ExistentialAnyWithCommonPluginDependencyMigration/Package.swift b/Fixtures/SwiftMigrate/ExistentialAnyWithCommonPluginDependencyMigration/Package.swift new file mode 100644 index 00000000000..1cc039b9215 --- /dev/null +++ b/Fixtures/SwiftMigrate/ExistentialAnyWithCommonPluginDependencyMigration/Package.swift @@ -0,0 +1,16 @@ +// swift-tools-version:5.8 + +import PackageDescription + +let package = Package( + name: "ExistentialAnyMigration", + platforms: [ + .macOS(.v10_15) + ], + targets: [ + .target(name: "Library", dependencies: ["CommonLibrary"], plugins: [.plugin(name: "Plugin")]), + .plugin(name: "Plugin", capability: .buildTool, dependencies: ["Tool"]), + .executableTarget(name: "Tool", dependencies: ["CommonLibrary"]), + .target(name: "CommonLibrary"), + ] +) diff --git a/Fixtures/SwiftMigrate/ExistentialAnyWithCommonPluginDependencyMigration/Plugins/Plugin/Plugin.swift b/Fixtures/SwiftMigrate/ExistentialAnyWithCommonPluginDependencyMigration/Plugins/Plugin/Plugin.swift new file mode 100644 index 00000000000..2410af9dbef --- /dev/null +++ b/Fixtures/SwiftMigrate/ExistentialAnyWithCommonPluginDependencyMigration/Plugins/Plugin/Plugin.swift @@ -0,0 +1,17 @@ +import PackagePlugin +import Foundation + +@main struct Plugin: BuildToolPlugin { + func createBuildCommands(context: PluginContext, target: Target) throws -> [Command] { + let tool = try context.tool(named: "Tool") + let output = context.pluginWorkDirectory.appending(["generated.swift"]) + return [ + .buildCommand( + displayName: "Plugin", + executable: tool.path, + arguments: [output], + inputFiles: [], + outputFiles: [output]) + ] + } +} diff --git a/Fixtures/SwiftMigrate/ExistentialAnyWithCommonPluginDependencyMigration/Sources/CommonLibrary/Common.swift b/Fixtures/SwiftMigrate/ExistentialAnyWithCommonPluginDependencyMigration/Sources/CommonLibrary/Common.swift new file mode 100644 index 00000000000..4f6ab41fc9b --- /dev/null +++ b/Fixtures/SwiftMigrate/ExistentialAnyWithCommonPluginDependencyMigration/Sources/CommonLibrary/Common.swift @@ -0,0 +1,6 @@ +public func common() {} + + +protocol P {} + +func needsMigration(_ p: P) {} diff --git a/Fixtures/SwiftMigrate/ExistentialAnyWithCommonPluginDependencyMigration/Sources/Library/Test.swift b/Fixtures/SwiftMigrate/ExistentialAnyWithCommonPluginDependencyMigration/Sources/Library/Test.swift new file mode 100644 index 00000000000..bc8a65d8348 --- /dev/null +++ b/Fixtures/SwiftMigrate/ExistentialAnyWithCommonPluginDependencyMigration/Sources/Library/Test.swift @@ -0,0 +1,6 @@ +import CommonLibrary + +func bar() { + generatedFunction() + common() +} diff --git a/Fixtures/SwiftMigrate/ExistentialAnyWithCommonPluginDependencyMigration/Sources/Tool/tool.swift b/Fixtures/SwiftMigrate/ExistentialAnyWithCommonPluginDependencyMigration/Sources/Tool/tool.swift new file mode 100644 index 00000000000..e69e49865a4 --- /dev/null +++ b/Fixtures/SwiftMigrate/ExistentialAnyWithCommonPluginDependencyMigration/Sources/Tool/tool.swift @@ -0,0 +1,13 @@ +import Foundation +import CommonLibrary + +@main struct Entry { + public static func main() async throws { + common() + let outputPath = CommandLine.arguments[1] + let contents = """ + func generatedFunction() {} + """ + FileManager.default.createFile(atPath: outputPath, contents: contents.data(using: .utf8)) + } +} diff --git a/Fixtures/SwiftMigrate/ExistentialAnyWithPluginMigration/Package.swift b/Fixtures/SwiftMigrate/ExistentialAnyWithPluginMigration/Package.swift new file mode 100644 index 00000000000..0c6a2961f58 --- /dev/null +++ b/Fixtures/SwiftMigrate/ExistentialAnyWithPluginMigration/Package.swift @@ -0,0 +1,15 @@ +// swift-tools-version:5.8 + +import PackageDescription + +let package = Package( + name: "ExistentialAnyMigration", + platforms: [ + .macOS(.v10_15) + ], + targets: [ + .target(name: "Library", plugins: [.plugin(name: "Plugin")]), + .plugin(name: "Plugin", capability: .buildTool, dependencies: ["Tool"]), + .executableTarget(name: "Tool"), + ] +) diff --git a/Fixtures/SwiftMigrate/ExistentialAnyWithPluginMigration/Plugins/Plugin/Plugin.swift b/Fixtures/SwiftMigrate/ExistentialAnyWithPluginMigration/Plugins/Plugin/Plugin.swift new file mode 100644 index 00000000000..2410af9dbef --- /dev/null +++ b/Fixtures/SwiftMigrate/ExistentialAnyWithPluginMigration/Plugins/Plugin/Plugin.swift @@ -0,0 +1,17 @@ +import PackagePlugin +import Foundation + +@main struct Plugin: BuildToolPlugin { + func createBuildCommands(context: PluginContext, target: Target) throws -> [Command] { + let tool = try context.tool(named: "Tool") + let output = context.pluginWorkDirectory.appending(["generated.swift"]) + return [ + .buildCommand( + displayName: "Plugin", + executable: tool.path, + arguments: [output], + inputFiles: [], + outputFiles: [output]) + ] + } +} diff --git a/Fixtures/SwiftMigrate/ExistentialAnyWithPluginMigration/Sources/Library/Test.swift b/Fixtures/SwiftMigrate/ExistentialAnyWithPluginMigration/Sources/Library/Test.swift new file mode 100644 index 00000000000..dfae68a022d --- /dev/null +++ b/Fixtures/SwiftMigrate/ExistentialAnyWithPluginMigration/Sources/Library/Test.swift @@ -0,0 +1,20 @@ +protocol P { +} + +func test1(_: P) { +} + +func test2(_: P.Protocol) { +} + +func test3() { + let _: [P?] = [] +} + +func test4() { + var x = 42 +} + +func bar() { + generatedFunction() +} diff --git a/Fixtures/SwiftMigrate/ExistentialAnyWithPluginMigration/Sources/Tool/tool.swift b/Fixtures/SwiftMigrate/ExistentialAnyWithPluginMigration/Sources/Tool/tool.swift new file mode 100644 index 00000000000..18c38c1df83 --- /dev/null +++ b/Fixtures/SwiftMigrate/ExistentialAnyWithPluginMigration/Sources/Tool/tool.swift @@ -0,0 +1,12 @@ +import Foundation + +@main struct Entry { + public static func main() async throws { + let outputPath = CommandLine.arguments[1] + let contents = """ + func generatedFunction() {} + func dontmodifyme(_: P) {} + """ + FileManager.default.createFile(atPath: outputPath, contents: contents.data(using: .utf8)) + } +} diff --git a/Fixtures/SwiftMigrate/InferIsolatedConformancesMigration/Package.swift b/Fixtures/SwiftMigrate/InferIsolatedConformancesMigration/Package.swift new file mode 100644 index 00000000000..9ed6d9922b2 --- /dev/null +++ b/Fixtures/SwiftMigrate/InferIsolatedConformancesMigration/Package.swift @@ -0,0 +1,10 @@ +// swift-tools-version:6.2 + +import PackageDescription + +let package = Package( + name: "InferIsolatedConformancesMigration", + targets: [ + .target(name: "Diagnostics", path: "Sources", exclude: ["Fixed"]), + ] +) diff --git a/Fixtures/SwiftMigrate/InferIsolatedConformancesMigration/Sources/Fixed/Test.swift b/Fixtures/SwiftMigrate/InferIsolatedConformancesMigration/Sources/Fixed/Test.swift new file mode 100644 index 00000000000..924375fc0e2 --- /dev/null +++ b/Fixtures/SwiftMigrate/InferIsolatedConformancesMigration/Sources/Fixed/Test.swift @@ -0,0 +1,8 @@ +@MainActor +class C: nonisolated Equatable { + let name = "Hello" + + nonisolated static func ==(lhs: C, rhs: C) -> Bool { + lhs.name == rhs.name + } +} diff --git a/Fixtures/SwiftMigrate/InferIsolatedConformancesMigration/Sources/Fixed/Test2.swift b/Fixtures/SwiftMigrate/InferIsolatedConformancesMigration/Sources/Fixed/Test2.swift new file mode 100644 index 00000000000..f4e8671264f --- /dev/null +++ b/Fixtures/SwiftMigrate/InferIsolatedConformancesMigration/Sources/Fixed/Test2.swift @@ -0,0 +1,5 @@ +protocol P {} +protocol Q {} + +@MainActor +struct S: nonisolated P & Q {} diff --git a/Fixtures/SwiftMigrate/InferIsolatedConformancesMigration/Sources/Test.swift b/Fixtures/SwiftMigrate/InferIsolatedConformancesMigration/Sources/Test.swift new file mode 100644 index 00000000000..162d29df5ca --- /dev/null +++ b/Fixtures/SwiftMigrate/InferIsolatedConformancesMigration/Sources/Test.swift @@ -0,0 +1,8 @@ +@MainActor +class C: Equatable { + let name = "Hello" + + nonisolated static func ==(lhs: C, rhs: C) -> Bool { + lhs.name == rhs.name + } +} diff --git a/Fixtures/SwiftMigrate/InferIsolatedConformancesMigration/Sources/Test2.swift b/Fixtures/SwiftMigrate/InferIsolatedConformancesMigration/Sources/Test2.swift new file mode 100644 index 00000000000..b8980dfdaed --- /dev/null +++ b/Fixtures/SwiftMigrate/InferIsolatedConformancesMigration/Sources/Test2.swift @@ -0,0 +1,5 @@ +protocol P {} +protocol Q {} + +@MainActor +struct S: P & Q {} diff --git a/Fixtures/SwiftMigrate/StrictMemorySafetyMigration/Package.swift b/Fixtures/SwiftMigrate/StrictMemorySafetyMigration/Package.swift new file mode 100644 index 00000000000..b8301a1add9 --- /dev/null +++ b/Fixtures/SwiftMigrate/StrictMemorySafetyMigration/Package.swift @@ -0,0 +1,10 @@ +// swift-tools-version:6.2 + +import PackageDescription + +let package = Package( + name: "StrictMemorySafetyMigration", + targets: [ + .target(name: "Diagnostics", path: "Sources", exclude: ["Fixed"]), + ] +) diff --git a/Fixtures/SwiftMigrate/StrictMemorySafetyMigration/Sources/Fixed/Test.swift b/Fixtures/SwiftMigrate/StrictMemorySafetyMigration/Sources/Fixed/Test.swift new file mode 100644 index 00000000000..d0862663c49 --- /dev/null +++ b/Fixtures/SwiftMigrate/StrictMemorySafetyMigration/Sources/Fixed/Test.swift @@ -0,0 +1,5 @@ +@unsafe func f() { } + +func g() { + unsafe f() +} diff --git a/Fixtures/SwiftMigrate/StrictMemorySafetyMigration/Sources/Test.swift b/Fixtures/SwiftMigrate/StrictMemorySafetyMigration/Sources/Test.swift new file mode 100644 index 00000000000..8b7defd4323 --- /dev/null +++ b/Fixtures/SwiftMigrate/StrictMemorySafetyMigration/Sources/Test.swift @@ -0,0 +1,5 @@ +@unsafe func f() { } + +func g() { + f() +} diff --git a/Fixtures/SwiftMigrate/UpdateManifest/Package.swift b/Fixtures/SwiftMigrate/UpdateManifest/Package.swift new file mode 100644 index 00000000000..fedb00e411e --- /dev/null +++ b/Fixtures/SwiftMigrate/UpdateManifest/Package.swift @@ -0,0 +1,24 @@ +// swift-tools-version:5.8 + +import PackageDescription + +var swiftSettings: [SwiftSetting] = [] + +let package = Package( + name: "WithErrors", + targets: [ + .target( + name: "CannotFindSettings", + swiftSettings: swiftSettings + ), + .target(name: "A"), + .target(name: "B"), + ] +) + +package.targets.append( + .target( + name: "CannotFindTarget", + swiftSettings: swiftSettings + ), +) diff --git a/Fixtures/SwiftMigrate/UpdateManifest/Package.updated.targets-A-B.swift b/Fixtures/SwiftMigrate/UpdateManifest/Package.updated.targets-A-B.swift new file mode 100644 index 00000000000..420622ed6c6 --- /dev/null +++ b/Fixtures/SwiftMigrate/UpdateManifest/Package.updated.targets-A-B.swift @@ -0,0 +1,28 @@ +// swift-tools-version:5.8 + +import PackageDescription + +var swiftSettings: [SwiftSetting] = [] + +let package = Package( + name: "WithErrors", + targets: [ + .target( + name: "CannotFindSettings", + swiftSettings: swiftSettings + ), + .target(name: "A",swiftSettings: [ + .enableUpcomingFeature("ExistentialAny"), + .enableUpcomingFeature("InferIsolatedConformances"),]), + .target(name: "B",swiftSettings: [ + .enableUpcomingFeature("ExistentialAny"), + .enableUpcomingFeature("InferIsolatedConformances"),]), + ] +) + +package.targets.append( + .target( + name: "CannotFindTarget", + swiftSettings: swiftSettings + ), +) diff --git a/Fixtures/SwiftMigrate/UpdateManifest/Package.updated.targets-A.swift b/Fixtures/SwiftMigrate/UpdateManifest/Package.updated.targets-A.swift new file mode 100644 index 00000000000..6502c0cbe66 --- /dev/null +++ b/Fixtures/SwiftMigrate/UpdateManifest/Package.updated.targets-A.swift @@ -0,0 +1,26 @@ +// swift-tools-version:5.8 + +import PackageDescription + +var swiftSettings: [SwiftSetting] = [] + +let package = Package( + name: "WithErrors", + targets: [ + .target( + name: "CannotFindSettings", + swiftSettings: swiftSettings + ), + .target(name: "A",swiftSettings: [ + .enableUpcomingFeature("ExistentialAny"), + .enableUpcomingFeature("InferIsolatedConformances"),]), + .target(name: "B"), + ] +) + +package.targets.append( + .target( + name: "CannotFindTarget", + swiftSettings: swiftSettings + ), +) diff --git a/Fixtures/SwiftMigrate/UpdateManifest/Package.updated.targets-all.swift b/Fixtures/SwiftMigrate/UpdateManifest/Package.updated.targets-all.swift new file mode 100644 index 00000000000..420622ed6c6 --- /dev/null +++ b/Fixtures/SwiftMigrate/UpdateManifest/Package.updated.targets-all.swift @@ -0,0 +1,28 @@ +// swift-tools-version:5.8 + +import PackageDescription + +var swiftSettings: [SwiftSetting] = [] + +let package = Package( + name: "WithErrors", + targets: [ + .target( + name: "CannotFindSettings", + swiftSettings: swiftSettings + ), + .target(name: "A",swiftSettings: [ + .enableUpcomingFeature("ExistentialAny"), + .enableUpcomingFeature("InferIsolatedConformances"),]), + .target(name: "B",swiftSettings: [ + .enableUpcomingFeature("ExistentialAny"), + .enableUpcomingFeature("InferIsolatedConformances"),]), + ] +) + +package.targets.append( + .target( + name: "CannotFindTarget", + swiftSettings: swiftSettings + ), +) diff --git a/Fixtures/SwiftMigrate/UpdateManifest/Sources/A/File.swift b/Fixtures/SwiftMigrate/UpdateManifest/Sources/A/File.swift new file mode 100644 index 00000000000..6d71fe5d9ce --- /dev/null +++ b/Fixtures/SwiftMigrate/UpdateManifest/Sources/A/File.swift @@ -0,0 +1 @@ +public func foo() {} diff --git a/Fixtures/SwiftMigrate/UpdateManifest/Sources/B/File.swift b/Fixtures/SwiftMigrate/UpdateManifest/Sources/B/File.swift new file mode 100644 index 00000000000..6d71fe5d9ce --- /dev/null +++ b/Fixtures/SwiftMigrate/UpdateManifest/Sources/B/File.swift @@ -0,0 +1 @@ +public func foo() {} diff --git a/Fixtures/SwiftMigrate/UpdateManifest/Sources/CannotFindSettings/File.swift b/Fixtures/SwiftMigrate/UpdateManifest/Sources/CannotFindSettings/File.swift new file mode 100644 index 00000000000..6d71fe5d9ce --- /dev/null +++ b/Fixtures/SwiftMigrate/UpdateManifest/Sources/CannotFindSettings/File.swift @@ -0,0 +1 @@ +public func foo() {} diff --git a/Fixtures/SwiftMigrate/UpdateManifest/Sources/CannotFindTarget/File.swift b/Fixtures/SwiftMigrate/UpdateManifest/Sources/CannotFindTarget/File.swift new file mode 100644 index 00000000000..6d71fe5d9ce --- /dev/null +++ b/Fixtures/SwiftMigrate/UpdateManifest/Sources/CannotFindTarget/File.swift @@ -0,0 +1 @@ +public func foo() {} diff --git a/Fixtures/SwiftSDKs/Package.swift b/Fixtures/SwiftSDKs/Package.swift new file mode 100644 index 00000000000..fc9bc8d91c8 --- /dev/null +++ b/Fixtures/SwiftSDKs/Package.swift @@ -0,0 +1 @@ +// This empty file tells test fixture logic to copy this directory's content to the test case temp directory. diff --git a/Fixtures/SwiftSDKs/test-sdk.artifactbundle.tar.gz b/Fixtures/SwiftSDKs/test-sdk.artifactbundle.tar.gz index 04f705d0b65..d02b411724d 100644 Binary files a/Fixtures/SwiftSDKs/test-sdk.artifactbundle.tar.gz and b/Fixtures/SwiftSDKs/test-sdk.artifactbundle.tar.gz differ diff --git a/Fixtures/SwiftSDKs/test-sdk.artifactbundle.zip b/Fixtures/SwiftSDKs/test-sdk.artifactbundle.zip new file mode 100644 index 00000000000..6d38f278a20 Binary files /dev/null and b/Fixtures/SwiftSDKs/test-sdk.artifactbundle.zip differ diff --git a/Fixtures/Traits/DisablingEmptyDefaultsExample/Package.swift b/Fixtures/Traits/DisablingEmptyDefaultsExample/Package.swift new file mode 100644 index 00000000000..e9fd984c807 --- /dev/null +++ b/Fixtures/Traits/DisablingEmptyDefaultsExample/Package.swift @@ -0,0 +1,18 @@ +// swift-tools-version: 6.1 + +import PackageDescription + +let package = Package( + name: "DisablingEmptyDefaultsExample", + dependencies: [ + .package( + path: "../Package11", + traits: [] + ), + ], + targets: [ + .executableTarget( + name: "DisablingEmptyDefaultsExample" + ), + ] +) diff --git a/Fixtures/Traits/DisablingEmptyDefaultsExample/Sources/DisablingEmptyDefaultsExample/Example.swift b/Fixtures/Traits/DisablingEmptyDefaultsExample/Sources/DisablingEmptyDefaultsExample/Example.swift new file mode 100644 index 00000000000..b8e4bbeb369 --- /dev/null +++ b/Fixtures/Traits/DisablingEmptyDefaultsExample/Sources/DisablingEmptyDefaultsExample/Example.swift @@ -0,0 +1,6 @@ +@main +struct Example { + static func main() { + + } +} \ No newline at end of file diff --git a/Fixtures/Traits/Example/Package.swift b/Fixtures/Traits/Example/Package.swift new file mode 100644 index 00000000000..b5358cb9d3b --- /dev/null +++ b/Fixtures/Traits/Example/Package.swift @@ -0,0 +1,168 @@ +// swift-tools-version: 6.1 + +import PackageDescription + +let package = Package( + name: "TraitsExample", + traits: [ + .default( + enabledTraits: [ + "Package1", + "Package2", + "Package3", + "Package4", + "BuildCondition1", + ] + ), + "Package1", + "Package2", + "Package3", + "Package4", + "Package5", + "Package7", + "Package9", + "Package10", + "BuildCondition1", + "BuildCondition2", + "BuildCondition3", + "ExtraTrait", + ], + dependencies: [ + .package( + path: "../Package1", + traits: ["Package1Trait1"] + ), + .package( + path: "../Package2", + traits: ["Package2Trait1"] + ), + .package( + path: "../Package3" + ), + .package( + path: "../Package4", + traits: [] + ), + .package( + path: "../Package5", + traits: ["Package5Trait1"] + ), + .package( + path: "../Package7" + ), + .package( + path: "../Package9" + ), + .package( + path: "../Package10", + traits: ["Package10Trait2"] + ), + ], + targets: [ + .executableTarget( + name: "Example", + dependencies: [ + .product( + name: "Package1Library1", + package: "Package1", + condition: .when(traits: ["Package1"]) + ), + .product( + name: "Package2Library1", + package: "Package2", + condition: .when(traits: ["Package2"]) + ), + .product( + name: "Package3Library1", + package: "Package3", + condition: .when(traits: ["Package3"]) + ), + .product( + name: "Package4Library1", + package: "Package4", + condition: .when(traits: ["Package4"]) + ), + .product( + name: "Package5Library1", + package: "Package5", + condition: .when(traits: ["Package5"]) + ), + .product( + name: "Package7Library1", + package: "Package7", + condition: .when(traits: ["Package7"]) + ), + .product( + name: "Package9Library1", + package: "Package9", + condition: .when(traits: ["Package9"]) + ), + .product( + name: "Package10Library1", + package: "Package10", + condition: .when(traits: ["Package10"]) + ), + .product( + name: "Package10Library2", + package: "Package10", + condition: .when(traits: ["Package10", "ExtraTrait"]) + ) + ], + swiftSettings: [ + .define("DEFINE1", .when(traits: ["BuildCondition1"])), + .define("DEFINE2", .when(traits: ["BuildCondition2"])), + .define("DEFINE3", .when(traits: ["BuildCondition3"])), + ] + ), + .testTarget( + name: "ExampleTests", + dependencies: [ + .product( + name: "Package1Library1", + package: "Package1", + condition: .when(traits: ["Package1"]) + ), + .product( + name: "Package2Library1", + package: "Package2", + condition: .when(traits: ["Package2"]) + ), + .product( + name: "Package3Library1", + package: "Package3", + condition: .when(traits: ["Package3"]) + ), + .product( + name: "Package4Library1", + package: "Package4", + condition: .when(traits: ["Package4"]) + ), + .product( + name: "Package5Library1", + package: "Package5", + condition: .when(traits: ["Package5"]) + ), + .product( + name: "Package7Library1", + package: "Package7", + condition: .when(traits: ["Package7"]) + ), + .product( + name: "Package9Library1", + package: "Package9", + condition: .when(traits: ["Package9"]) + ), + .product( + name: "Package10Library1", + package: "Package10", + condition: .when(traits: ["Package10"]) + ), + ], + swiftSettings: [ + .define("DEFINE1", .when(traits: ["BuildCondition1"])), + .define("DEFINE2", .when(traits: ["BuildCondition2"])), + .define("DEFINE3", .when(traits: ["BuildCondition3"])), + ] + ) + ] +) diff --git a/Fixtures/Traits/Example/Sources/Example/Example.swift b/Fixtures/Traits/Example/Sources/Example/Example.swift new file mode 100644 index 00000000000..d1edafb6bb2 --- /dev/null +++ b/Fixtures/Traits/Example/Sources/Example/Example.swift @@ -0,0 +1,77 @@ +#if Package1 +import Package1Library1 +#endif +#if Package2 +import Package2Library1 +#endif +#if Package3 +import Package3Library1 +#endif +#if Package4 +import Package4Library1 +#endif +#if Package5 +import Package5Library1 +#endif +#if Package7 +import Package7Library1 +#endif +#if Package9 +import Package9Library1 +#endif +#if Package10 +import Package10Library1 +import Package10Library2 +#endif +#if ExtraTrait +import Package10Library2 +#endif + +@main +struct Example { + static func main() { + #if Package1 + Package1Library1.hello() + #endif + #if Package2 + Package2Library1.hello() + #endif + #if Package3 + Package3Library1.hello() + #endif + #if Package4 + Package4Library1.hello() + #endif + #if Package5 + Package5Library1.hello() + #endif + #if Package7 + Package7Library1.hello() + #endif + #if Package9 + Package9Library1.hello() + #endif + #if Package10 + Package10Library1.hello() + Package10Library2.hello() + #endif + #if ExtraTrait + Package10Library2.hello() + #endif + #if DEFINE1 + print("DEFINE1 enabled") + #else + print("DEFINE1 disabled") + #endif + #if DEFINE2 + print("DEFINE2 enabled") + #else + print("DEFINE2 disabled") + #endif + #if DEFINE3 + print("DEFINE3 enabled") + #else + print("DEFINE3 disabled") + #endif + } +} diff --git a/Fixtures/Traits/Example/Tests/ExampleTests/Tests.swift b/Fixtures/Traits/Example/Tests/ExampleTests/Tests.swift new file mode 100644 index 00000000000..73caf1aeb44 --- /dev/null +++ b/Fixtures/Traits/Example/Tests/ExampleTests/Tests.swift @@ -0,0 +1,70 @@ +#if Package1 +import Package1Library1 +#endif +#if Package2 +import Package2Library1 +#endif +#if Package3 +import Package3Library1 +#endif +#if Package4 +import Package4Library1 +#endif +#if Package5 +import Package5Library1 +#endif +#if Package7 +import Package7Library1 +#endif +#if Package9 +import Package9Library1 +#endif +#if Package10 +import Package10Library1 +#endif + +import XCTest + +final class Tests: XCTestCase { + func testTraits() { + #if Package1 + Package1Library1.hello() + #endif + #if Package2 + Package2Library1.hello() + #endif + #if Package3 + Package3Library1.hello() + #endif + #if Package4 + Package4Library1.hello() + #endif + #if Package5 + Package5Library1.hello() + #endif + #if Package7 + Package7Library1.hello() + #endif + #if Package9 + Package9Library1.hello() + #endif + #if Package10 + Package10Library1.hello() + #endif + #if DEFINE1 + print("DEFINE1 enabled") + #else + print("DEFINE1 disabled") + #endif + #if DEFINE2 + print("DEFINE2 enabled") + #else + print("DEFINE2 disabled") + #endif + #if DEFINE3 + print("DEFINE3 enabled") + #else + print("DEFINE3 disabled") + #endif + } +} \ No newline at end of file diff --git a/Fixtures/Traits/Package1/Package.swift b/Fixtures/Traits/Package1/Package.swift new file mode 100644 index 00000000000..f90fcdc72e5 --- /dev/null +++ b/Fixtures/Traits/Package1/Package.swift @@ -0,0 +1,21 @@ +// swift-tools-version: 6.1 + +import PackageDescription + +let package = Package( + name: "Package1", + products: [ + .library( + name: "Package1Library1", + targets: ["Package1Library1"] + ), + ], + traits: [ + "Package1Trait1" + ], + targets: [ + .target( + name: "Package1Library1" + ), + ] +) diff --git a/Fixtures/Traits/Package1/Sources/Package1Library1/Library.swift b/Fixtures/Traits/Package1/Sources/Package1Library1/Library.swift new file mode 100644 index 00000000000..f5497881355 --- /dev/null +++ b/Fixtures/Traits/Package1/Sources/Package1Library1/Library.swift @@ -0,0 +1,7 @@ +public func hello() { + #if Package1Trait1 + print("Package1Library1 trait1 enabled") + #else + print("Package1Library1 trait1 disabled") + #endif +} diff --git a/Fixtures/Traits/Package10/Package.swift b/Fixtures/Traits/Package10/Package.swift new file mode 100644 index 00000000000..60767db5e7a --- /dev/null +++ b/Fixtures/Traits/Package10/Package.swift @@ -0,0 +1,35 @@ +// swift-tools-version: 6.1 + +import PackageDescription + +let package = Package( + name: "Package10", + products: [ + .library( + name: "Package10Library1", + targets: ["Package10Library1"] + ), + .library( + name: "Package10Library2", + targets: ["Package10Library2"] + ), + ], + traits: [ + "Package10Trait1", + "Package10Trait2" + ], + targets: [ + .target( + name: "Package10Library1" + ), + .target( + name: "Package10Library2" + ), + .plugin( + name: "SymbolGraphExtract", + capability: .command( + intent: .custom(verb: "extract", description: "") + ) + ), + ] +) diff --git a/Fixtures/Traits/Package10/Plugins/SymbolGraphExtract/Plugin.swift b/Fixtures/Traits/Package10/Plugins/SymbolGraphExtract/Plugin.swift new file mode 100644 index 00000000000..9e4be759525 --- /dev/null +++ b/Fixtures/Traits/Package10/Plugins/SymbolGraphExtract/Plugin.swift @@ -0,0 +1,11 @@ +import PackagePlugin + +@main struct SymbolGraphExtractPlugin: CommandPlugin { + func performCommand( + context: PluginContext, + arguments: [String] + ) throws { + let result = try self.packageManager.getSymbolGraph(for: context.package.targets.first!, options: .init()) + print(result.directoryPath) + } +} \ No newline at end of file diff --git a/Fixtures/Traits/Package10/Sources/Package10Library1/Library.swift b/Fixtures/Traits/Package10/Sources/Package10Library1/Library.swift new file mode 100644 index 00000000000..9319c1d9e5b --- /dev/null +++ b/Fixtures/Traits/Package10/Sources/Package10Library1/Library.swift @@ -0,0 +1,20 @@ +public func hello() { + #if Package10Trait1 + print("Package10Library1 trait1 enabled") + #else + print("Package10Library1 trait1 disabled") + #endif + #if Package10Trait2 + print("Package10Library1 trait2 enabled") + #else + print("Package10Library1 trait2 disabled") + #endif +} + +#if Package10Trait1 +public struct TypeGatedByPackage10Trait1 {} +#endif + +#if Package10Trait2 +public struct TypeGatedByPackage10Trait2 {} +#endif \ No newline at end of file diff --git a/Fixtures/Traits/Package10/Sources/Package10Library2/Library.swift b/Fixtures/Traits/Package10/Sources/Package10Library2/Library.swift new file mode 100644 index 00000000000..8d1dd343c75 --- /dev/null +++ b/Fixtures/Traits/Package10/Sources/Package10Library2/Library.swift @@ -0,0 +1,3 @@ +public func hello() { + print("Package10Library2 has been included.") +} \ No newline at end of file diff --git a/Fixtures/Traits/Package11/Package.swift b/Fixtures/Traits/Package11/Package.swift new file mode 100644 index 00000000000..9e2446bcc61 --- /dev/null +++ b/Fixtures/Traits/Package11/Package.swift @@ -0,0 +1,18 @@ +// swift-tools-version: 6.1 + +import PackageDescription + +let package = Package( + name: "Package11", + products: [ + .library( + name: "Package11Library1", + targets: ["Package11Library1"] + ), + ], + targets: [ + .target( + name: "Package11Library1" + ), + ] +) diff --git a/Fixtures/Traits/Package11/Sources/Package11Library1/Library.swift b/Fixtures/Traits/Package11/Sources/Package11Library1/Library.swift new file mode 100644 index 00000000000..2fc0036893f --- /dev/null +++ b/Fixtures/Traits/Package11/Sources/Package11Library1/Library.swift @@ -0,0 +1,3 @@ +public func hello() { + print("Package11Library1") +} \ No newline at end of file diff --git a/Fixtures/Traits/Package2/Package.swift b/Fixtures/Traits/Package2/Package.swift new file mode 100644 index 00000000000..a1db5968571 --- /dev/null +++ b/Fixtures/Traits/Package2/Package.swift @@ -0,0 +1,22 @@ +// swift-tools-version: 6.1 + +import PackageDescription + +let package = Package( + name: "Package2", + products: [ + .library( + name: "Package2Library1", + targets: ["Package2Library1"] + ), + ], + traits: [ + Trait(name: "Package2Trait1", enabledTraits: ["Package2Trait2"]), + "Package2Trait2", + ], + targets: [ + .target( + name: "Package2Library1" + ), + ] +) diff --git a/Fixtures/Traits/Package2/Sources/Package2Library1/Library.swift b/Fixtures/Traits/Package2/Sources/Package2Library1/Library.swift new file mode 100644 index 00000000000..42e2bbbedb7 --- /dev/null +++ b/Fixtures/Traits/Package2/Sources/Package2Library1/Library.swift @@ -0,0 +1,7 @@ +public func hello() { + #if Package2Trait2 + print("Package2Library1 trait2 enabled") + #else + print("Package2Library1 trait2 disabled") + #endif +} diff --git a/Fixtures/Traits/Package3/Package.swift b/Fixtures/Traits/Package3/Package.swift new file mode 100644 index 00000000000..dee1273e9a6 --- /dev/null +++ b/Fixtures/Traits/Package3/Package.swift @@ -0,0 +1,24 @@ +// swift-tools-version: 6.1 + +import PackageDescription + +let package = Package( + name: "Package3", + products: [ + .library( + name: "Package3Library1", + targets: ["Package3Library1"] + ), + ], + traits: [ + .default(enabledTraits: ["Package3Trait3"]), + .trait(name: "Package3Trait1", enabledTraits: ["Package3Trait2"]), + .trait(name: "Package3Trait2", enabledTraits: ["Package3Trait3"]), + "Package3Trait3", + ], + targets: [ + .target( + name: "Package3Library1" + ), + ] +) diff --git a/Fixtures/Traits/Package3/Sources/Package3Library1/Library.swift b/Fixtures/Traits/Package3/Sources/Package3Library1/Library.swift new file mode 100644 index 00000000000..041bab2a1df --- /dev/null +++ b/Fixtures/Traits/Package3/Sources/Package3Library1/Library.swift @@ -0,0 +1,7 @@ +public func hello() { + #if Package3Trait3 + print("Package3Library1 trait3 enabled") + #else + print("Package3Library1 trait3 disabled") + #endif +} diff --git a/Fixtures/Traits/Package4/Package.swift b/Fixtures/Traits/Package4/Package.swift new file mode 100644 index 00000000000..55f86d5ded4 --- /dev/null +++ b/Fixtures/Traits/Package4/Package.swift @@ -0,0 +1,22 @@ +// swift-tools-version: 6.1 + +import PackageDescription + +let package = Package( + name: "Package4", + products: [ + .library( + name: "Package4Library1", + targets: ["Package4Library1"] + ), + ], + traits: [ + .default(enabledTraits: ["Package4Trait1"]), + "Package4Trait1", + ], + targets: [ + .target( + name: "Package4Library1" + ), + ] +) diff --git a/Fixtures/Traits/Package4/Sources/Package4Library1/Library.swift b/Fixtures/Traits/Package4/Sources/Package4Library1/Library.swift new file mode 100644 index 00000000000..c79fd748e7b --- /dev/null +++ b/Fixtures/Traits/Package4/Sources/Package4Library1/Library.swift @@ -0,0 +1,7 @@ +public func hello() { + #if Package4Trait1 + print("Package4Library1 trait1 enabled") + #else + print("Package4Library1 trait1 disabled") + #endif +} diff --git a/Fixtures/Traits/Package5/Package.swift b/Fixtures/Traits/Package5/Package.swift new file mode 100644 index 00000000000..35e34074ef8 --- /dev/null +++ b/Fixtures/Traits/Package5/Package.swift @@ -0,0 +1,35 @@ +// swift-tools-version: 6.1 + +import PackageDescription + +let package = Package( + name: "Package5", + products: [ + .library( + name: "Package5Library1", + targets: ["Package5Library1"] + ), + ], + traits: [ + "Package5Trait1" + ], + dependencies: [ + .package( + path: "../Package6", + traits: [ + Package.Dependency.Trait(name: "Package6Trait1", condition: .when(traits: ["Package5Trait1"])) + ] + ) + ], + targets: [ + .target( + name: "Package5Library1", + dependencies: [ + .product( + name: "Package6Library1", + package: "Package6" + ) + ] + ), + ] +) diff --git a/Fixtures/Traits/Package5/Sources/Package5Library1/Library.swift b/Fixtures/Traits/Package5/Sources/Package5Library1/Library.swift new file mode 100644 index 00000000000..4547568225d --- /dev/null +++ b/Fixtures/Traits/Package5/Sources/Package5Library1/Library.swift @@ -0,0 +1,12 @@ +#if Package5Trait1 +import Package6Library1 +#endif + +public func hello() { + #if Package5Trait1 + print("Package5Library1 trait1 enabled") + Package6Library1.hello() + #else + print("Package5Library1 trait1 disabled") + #endif +} diff --git a/Fixtures/Traits/Package6/Package.swift b/Fixtures/Traits/Package6/Package.swift new file mode 100644 index 00000000000..0d677d9ccf4 --- /dev/null +++ b/Fixtures/Traits/Package6/Package.swift @@ -0,0 +1,21 @@ +// swift-tools-version: 6.1 + +import PackageDescription + +let package = Package( + name: "Package6", + products: [ + .library( + name: "Package6Library1", + targets: ["Package6Library1"] + ), + ], + traits: [ + "Package6Trait1" + ], + targets: [ + .target( + name: "Package6Library1" + ), + ] +) diff --git a/Fixtures/Traits/Package6/Sources/Package6Library1/Library.swift b/Fixtures/Traits/Package6/Sources/Package6Library1/Library.swift new file mode 100644 index 00000000000..f92779bbd78 --- /dev/null +++ b/Fixtures/Traits/Package6/Sources/Package6Library1/Library.swift @@ -0,0 +1,7 @@ +public func hello() { + #if Package6Trait1 + print("Package6Library1 trait1 enabled") + #else + print("Package6Library1 trait1 disabled") + #endif +} diff --git a/Fixtures/Traits/Package7/Package.swift b/Fixtures/Traits/Package7/Package.swift new file mode 100644 index 00000000000..210aa90b91b --- /dev/null +++ b/Fixtures/Traits/Package7/Package.swift @@ -0,0 +1,33 @@ +// swift-tools-version: 6.1 + +import PackageDescription + +let package = Package( + name: "Package7", + products: [ + .library( + name: "Package7Library1", + targets: ["Package7Library1"] + ), + ], + traits: [ + "Package7Trait1" + ], + dependencies: [ + .package( + path: "../Package8" + ) + ], + targets: [ + .target( + name: "Package7Library1", + dependencies: [ + .product( + name: "Package8Library1", + package: "Package8", + condition: .when(traits: ["Package7Trait1"]) + ) + ] + ), + ] +) diff --git a/Fixtures/Traits/Package7/Sources/Package7Library1/Library.swift b/Fixtures/Traits/Package7/Sources/Package7Library1/Library.swift new file mode 100644 index 00000000000..fb74b92a69a --- /dev/null +++ b/Fixtures/Traits/Package7/Sources/Package7Library1/Library.swift @@ -0,0 +1,12 @@ +#if Package7Trait1 +import Package8Library1 +#endif + +public func hello() { + #if Package7Trait1 + print("Package7Library1 trait1 enabled") + Package8Library1.hello() + #else + print("Package7Library1 trait1 disabled") + #endif +} diff --git a/Fixtures/Traits/Package8/Package.swift b/Fixtures/Traits/Package8/Package.swift new file mode 100644 index 00000000000..6731b7ed4a7 --- /dev/null +++ b/Fixtures/Traits/Package8/Package.swift @@ -0,0 +1,21 @@ +// swift-tools-version: 6.1 + +import PackageDescription + +let package = Package( + name: "Package8", + products: [ + .library( + name: "Package8Library1", + targets: ["Package8Library1"] + ), + ], + traits: [ + "Package8Trait1" + ], + targets: [ + .target( + name: "Package8Library1" + ), + ] +) diff --git a/Fixtures/Traits/Package8/Sources/Package6Library1/Library.swift b/Fixtures/Traits/Package8/Sources/Package6Library1/Library.swift new file mode 100644 index 00000000000..bc5427d75a0 --- /dev/null +++ b/Fixtures/Traits/Package8/Sources/Package6Library1/Library.swift @@ -0,0 +1,7 @@ +public func hello() { + #if Package8Trait1 + print("Package8Library1 trait1 enabled") + #else + print("Package8Library1 trait1 disabled") + #endif +} diff --git a/Fixtures/Traits/Package9/Package.swift b/Fixtures/Traits/Package9/Package.swift new file mode 100644 index 00000000000..2bb1516fef0 --- /dev/null +++ b/Fixtures/Traits/Package9/Package.swift @@ -0,0 +1,30 @@ +// swift-tools-version: 6.1 + +import PackageDescription + +let package = Package( + name: "Package9", + products: [ + .library( + name: "Package9Library1", + targets: ["Package9Library1"] + ), + ], + dependencies: [ + .package( + path: "../Package10", + traits: ["Package10Trait1"] + ) + ], + targets: [ + .target( + name: "Package9Library1", + dependencies: [ + .product( + name: "Package10Library1", + package: "Package10" + ) + ] + ), + ] +) diff --git a/Fixtures/Traits/Package9/Sources/Package9Library1/Library.swift b/Fixtures/Traits/Package9/Sources/Package9Library1/Library.swift new file mode 100644 index 00000000000..44f8d0ead95 --- /dev/null +++ b/Fixtures/Traits/Package9/Sources/Package9Library1/Library.swift @@ -0,0 +1,5 @@ +import Package10Library1 + +public func hello() { + Package10Library1.hello() +} diff --git a/Fixtures/Traits/PackageConditionalDeps/Package.swift b/Fixtures/Traits/PackageConditionalDeps/Package.swift new file mode 100644 index 00000000000..d5567dc8ffd --- /dev/null +++ b/Fixtures/Traits/PackageConditionalDeps/Package.swift @@ -0,0 +1,39 @@ +// swift-tools-version: 6.1 + +import PackageDescription + +let package = Package( + name: "PackageConditionalDeps", + products: [ + .library( + name: "PackageConditionalDeps", + targets: ["PackageConditionalDeps"] + ), + ], + traits: [ + .default(enabledTraits: ["EnablePackage1Dep"]), + "EnablePackage1Dep", + "EnablePackage2Dep" + ], + dependencies: [ + .package(path: "../Package1"), + .package(path: "../Package2"), + ], + targets: [ + .target( + name: "PackageConditionalDeps", + dependencies: [ + .product( + name: "Package1Library1", + package: "Package1", + condition: .when(traits: ["EnablePackage1Dep"]) + ), + .product( + name: "Package2Library1", + package: "Package2", + condition: .when(traits: ["EnablePackage2Dep"]) + ) + ] + ), + ] +) diff --git a/Fixtures/Traits/PackageConditionalDeps/Sources/PackageConditionalDeps/PackageConditionalDeps.swift b/Fixtures/Traits/PackageConditionalDeps/Sources/PackageConditionalDeps/PackageConditionalDeps.swift new file mode 100644 index 00000000000..231b3efb31b --- /dev/null +++ b/Fixtures/Traits/PackageConditionalDeps/Sources/PackageConditionalDeps/PackageConditionalDeps.swift @@ -0,0 +1,3 @@ +public func nothingHappens() { + // Do nothing. +} diff --git a/IntegrationTests/Fixtures/XCBuild/ExecutableProducts/Bar/Package.swift b/Fixtures/XCBuild/ExecutableProducts/Bar/Package.swift similarity index 100% rename from IntegrationTests/Fixtures/XCBuild/ExecutableProducts/Bar/Package.swift rename to Fixtures/XCBuild/ExecutableProducts/Bar/Package.swift diff --git a/IntegrationTests/Fixtures/XCBuild/ExecutableProducts/Bar/Sources/BarLib/BarLib.swift b/Fixtures/XCBuild/ExecutableProducts/Bar/Sources/BarLib/BarLib.swift similarity index 100% rename from IntegrationTests/Fixtures/XCBuild/ExecutableProducts/Bar/Sources/BarLib/BarLib.swift rename to Fixtures/XCBuild/ExecutableProducts/Bar/Sources/BarLib/BarLib.swift diff --git a/IntegrationTests/Fixtures/XCBuild/ExecutableProducts/Bar/Sources/bar/main.swift b/Fixtures/XCBuild/ExecutableProducts/Bar/Sources/bar/main.swift similarity index 100% rename from IntegrationTests/Fixtures/XCBuild/ExecutableProducts/Bar/Sources/bar/main.swift rename to Fixtures/XCBuild/ExecutableProducts/Bar/Sources/bar/main.swift diff --git a/IntegrationTests/Fixtures/XCBuild/ExecutableProducts/Bar/Sources/cbar/main.c b/Fixtures/XCBuild/ExecutableProducts/Bar/Sources/cbar/main.c similarity index 100% rename from IntegrationTests/Fixtures/XCBuild/ExecutableProducts/Bar/Sources/cbar/main.c rename to Fixtures/XCBuild/ExecutableProducts/Bar/Sources/cbar/main.c diff --git a/IntegrationTests/Fixtures/XCBuild/ExecutableProducts/Foo/Package.swift b/Fixtures/XCBuild/ExecutableProducts/Foo/Package.swift similarity index 100% rename from IntegrationTests/Fixtures/XCBuild/ExecutableProducts/Foo/Package.swift rename to Fixtures/XCBuild/ExecutableProducts/Foo/Package.swift diff --git a/IntegrationTests/Fixtures/XCBuild/ExecutableProducts/Foo/Sources/FooLib/FooLib.swift b/Fixtures/XCBuild/ExecutableProducts/Foo/Sources/FooLib/FooLib.swift similarity index 100% rename from IntegrationTests/Fixtures/XCBuild/ExecutableProducts/Foo/Sources/FooLib/FooLib.swift rename to Fixtures/XCBuild/ExecutableProducts/Foo/Sources/FooLib/FooLib.swift diff --git a/IntegrationTests/Fixtures/XCBuild/ExecutableProducts/Foo/Sources/cfoo/main.c b/Fixtures/XCBuild/ExecutableProducts/Foo/Sources/cfoo/main.c similarity index 100% rename from IntegrationTests/Fixtures/XCBuild/ExecutableProducts/Foo/Sources/cfoo/main.c rename to Fixtures/XCBuild/ExecutableProducts/Foo/Sources/cfoo/main.c diff --git a/IntegrationTests/Fixtures/XCBuild/ExecutableProducts/Foo/Sources/foo/main.swift b/Fixtures/XCBuild/ExecutableProducts/Foo/Sources/foo/main.swift similarity index 100% rename from IntegrationTests/Fixtures/XCBuild/ExecutableProducts/Foo/Sources/foo/main.swift rename to Fixtures/XCBuild/ExecutableProducts/Foo/Sources/foo/main.swift diff --git a/IntegrationTests/Fixtures/XCBuild/Libraries/Bar/Package.swift b/Fixtures/XCBuild/Libraries/Bar/Package.swift similarity index 100% rename from IntegrationTests/Fixtures/XCBuild/Libraries/Bar/Package.swift rename to Fixtures/XCBuild/Libraries/Bar/Package.swift diff --git a/IntegrationTests/Fixtures/XCBuild/Libraries/Bar/Sources/BarLib/BarLib.swift b/Fixtures/XCBuild/Libraries/Bar/Sources/BarLib/BarLib.swift similarity index 100% rename from IntegrationTests/Fixtures/XCBuild/Libraries/Bar/Sources/BarLib/BarLib.swift rename to Fixtures/XCBuild/Libraries/Bar/Sources/BarLib/BarLib.swift diff --git a/IntegrationTests/Fixtures/XCBuild/Libraries/Foo/Package.swift b/Fixtures/XCBuild/Libraries/Foo/Package.swift similarity index 100% rename from IntegrationTests/Fixtures/XCBuild/Libraries/Foo/Package.swift rename to Fixtures/XCBuild/Libraries/Foo/Package.swift diff --git a/IntegrationTests/Fixtures/XCBuild/Libraries/Foo/Sources/CFooLib/CFooLib.m b/Fixtures/XCBuild/Libraries/Foo/Sources/CFooLib/CFooLib.m similarity index 100% rename from IntegrationTests/Fixtures/XCBuild/Libraries/Foo/Sources/CFooLib/CFooLib.m rename to Fixtures/XCBuild/Libraries/Foo/Sources/CFooLib/CFooLib.m diff --git a/IntegrationTests/Fixtures/XCBuild/Libraries/Foo/Sources/CFooLib/include/CFooLib.h b/Fixtures/XCBuild/Libraries/Foo/Sources/CFooLib/include/CFooLib.h similarity index 100% rename from IntegrationTests/Fixtures/XCBuild/Libraries/Foo/Sources/CFooLib/include/CFooLib.h rename to Fixtures/XCBuild/Libraries/Foo/Sources/CFooLib/include/CFooLib.h diff --git a/IntegrationTests/Fixtures/XCBuild/Libraries/Foo/Sources/FooLib/FooLib.swift b/Fixtures/XCBuild/Libraries/Foo/Sources/FooLib/FooLib.swift similarity index 100% rename from IntegrationTests/Fixtures/XCBuild/Libraries/Foo/Sources/FooLib/FooLib.swift rename to Fixtures/XCBuild/Libraries/Foo/Sources/FooLib/FooLib.swift diff --git a/IntegrationTests/Fixtures/XCBuild/SystemTargets/Foo/Package.swift b/Fixtures/XCBuild/SystemTargets/Foo/Package.swift similarity index 100% rename from IntegrationTests/Fixtures/XCBuild/SystemTargets/Foo/Package.swift rename to Fixtures/XCBuild/SystemTargets/Foo/Package.swift diff --git a/IntegrationTests/Fixtures/XCBuild/SystemTargets/Foo/Sources/SystemLib/module.modulemap b/Fixtures/XCBuild/SystemTargets/Foo/Sources/SystemLib/module.modulemap similarity index 100% rename from IntegrationTests/Fixtures/XCBuild/SystemTargets/Foo/Sources/SystemLib/module.modulemap rename to Fixtures/XCBuild/SystemTargets/Foo/Sources/SystemLib/module.modulemap diff --git a/IntegrationTests/Fixtures/XCBuild/SystemTargets/Foo/Sources/foo/main.swift b/Fixtures/XCBuild/SystemTargets/Foo/Sources/foo/main.swift similarity index 100% rename from IntegrationTests/Fixtures/XCBuild/SystemTargets/Foo/Sources/foo/main.swift rename to Fixtures/XCBuild/SystemTargets/Foo/Sources/foo/main.swift diff --git a/IntegrationTests/Fixtures/XCBuild/SystemTargets/Inputs/libsys.c b/Fixtures/XCBuild/SystemTargets/Inputs/libsys.c similarity index 100% rename from IntegrationTests/Fixtures/XCBuild/SystemTargets/Inputs/libsys.c rename to Fixtures/XCBuild/SystemTargets/Inputs/libsys.c diff --git a/IntegrationTests/Fixtures/XCBuild/SystemTargets/Inputs/libsys.h b/Fixtures/XCBuild/SystemTargets/Inputs/libsys.h similarity index 100% rename from IntegrationTests/Fixtures/XCBuild/SystemTargets/Inputs/libsys.h rename to Fixtures/XCBuild/SystemTargets/Inputs/libsys.h diff --git a/IntegrationTests/Fixtures/XCBuild/SystemTargets/Inputs/libsys.pc b/Fixtures/XCBuild/SystemTargets/Inputs/libsys.pc similarity index 100% rename from IntegrationTests/Fixtures/XCBuild/SystemTargets/Inputs/libsys.pc rename to Fixtures/XCBuild/SystemTargets/Inputs/libsys.pc diff --git a/IntegrationTests/Fixtures/XCBuild/TestProducts/Bar/Package.swift b/Fixtures/XCBuild/TestProducts/Bar/Package.swift similarity index 100% rename from IntegrationTests/Fixtures/XCBuild/TestProducts/Bar/Package.swift rename to Fixtures/XCBuild/TestProducts/Bar/Package.swift diff --git a/IntegrationTests/Fixtures/XCBuild/TestProducts/Bar/Sources/BarLib/BarLib.m b/Fixtures/XCBuild/TestProducts/Bar/Sources/BarLib/BarLib.m similarity index 100% rename from IntegrationTests/Fixtures/XCBuild/TestProducts/Bar/Sources/BarLib/BarLib.m rename to Fixtures/XCBuild/TestProducts/Bar/Sources/BarLib/BarLib.m diff --git a/IntegrationTests/Fixtures/XCBuild/TestProducts/Bar/Sources/BarLib/include/BarLib.h b/Fixtures/XCBuild/TestProducts/Bar/Sources/BarLib/include/BarLib.h similarity index 100% rename from IntegrationTests/Fixtures/XCBuild/TestProducts/Bar/Sources/BarLib/include/BarLib.h rename to Fixtures/XCBuild/TestProducts/Bar/Sources/BarLib/include/BarLib.h diff --git a/IntegrationTests/Fixtures/XCBuild/TestProducts/Foo/Package.swift b/Fixtures/XCBuild/TestProducts/Foo/Package.swift similarity index 100% rename from IntegrationTests/Fixtures/XCBuild/TestProducts/Foo/Package.swift rename to Fixtures/XCBuild/TestProducts/Foo/Package.swift diff --git a/IntegrationTests/Fixtures/XCBuild/TestProducts/Foo/Sources/FooLib/FooLib.swift b/Fixtures/XCBuild/TestProducts/Foo/Sources/FooLib/FooLib.swift similarity index 100% rename from IntegrationTests/Fixtures/XCBuild/TestProducts/Foo/Sources/FooLib/FooLib.swift rename to Fixtures/XCBuild/TestProducts/Foo/Sources/FooLib/FooLib.swift diff --git a/IntegrationTests/Fixtures/XCBuild/TestProducts/Foo/Tests/CFooTests/tests.m b/Fixtures/XCBuild/TestProducts/Foo/Tests/CFooTests/tests.m similarity index 100% rename from IntegrationTests/Fixtures/XCBuild/TestProducts/Foo/Tests/CFooTests/tests.m rename to Fixtures/XCBuild/TestProducts/Foo/Tests/CFooTests/tests.m diff --git a/IntegrationTests/Fixtures/XCBuild/TestProducts/Foo/Tests/FooTests/FooTests.swift b/Fixtures/XCBuild/TestProducts/Foo/Tests/FooTests/FooTests.swift similarity index 100% rename from IntegrationTests/Fixtures/XCBuild/TestProducts/Foo/Tests/FooTests/FooTests.swift rename to Fixtures/XCBuild/TestProducts/Foo/Tests/FooTests/FooTests.swift diff --git a/IntegrationTests/Package.swift b/IntegrationTests/Package.swift deleted file mode 100644 index 08c0406db57..00000000000 --- a/IntegrationTests/Package.swift +++ /dev/null @@ -1,25 +0,0 @@ -// swift-tools-version:5.4 - -import PackageDescription - -let package = Package( - name: "IntegrationTests", - targets: [ - .testTarget(name: "IntegrationTests", dependencies: [ - .product(name: "SwiftToolsSupport-auto", package: "swift-tools-support-core"), - .product(name: "TSCTestSupport", package: "swift-tools-support-core") - ]), - ] -) - -import class Foundation.ProcessInfo - -if ProcessInfo.processInfo.environment["SWIFTCI_USE_LOCAL_DEPS"] == nil { - package.dependencies += [ - .package(url: "https://github.com/apple/swift-tools-support-core.git", .branch("main")), - ] -} else { - package.dependencies += [ - .package(name: "swift-tools-support-core", path: "../TSC"), - ] -} diff --git a/IntegrationTests/README.md b/IntegrationTests/README.md deleted file mode 100644 index b3f0715c066..00000000000 --- a/IntegrationTests/README.md +++ /dev/null @@ -1,13 +0,0 @@ -# Integration Tests - -Automated tests for validating the generated Swift snapshots behave correctly. - -## Usage - -The tests are run using the 'test-toolchain' Python script: - -``` -$ Utilities/test-toolchain -``` - -By default, the script runs tests against the currently installed toolchain. diff --git a/IntegrationTests/Tests/IntegrationTests/BasicTests.swift b/IntegrationTests/Tests/IntegrationTests/BasicTests.swift deleted file mode 100644 index 67b71358078..00000000000 --- a/IntegrationTests/Tests/IntegrationTests/BasicTests.swift +++ /dev/null @@ -1,379 +0,0 @@ -/* - This source file is part of the Swift.org open source project - - Copyright (c) 2014 - 2020 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 Swift project authors - */ - -import XCTest -import TSCBasic -import TSCTestSupport - -final class BasicTests: XCTestCase { - func testVersion() throws { - XCTAssertMatch(try sh(swift, "--version").stdout, .contains("Swift version")) - } - - func testExamplePackageDealer() throws { - try XCTSkipIf(isSelfHosted, "These packages don't use the latest runtime library, which doesn't work with self-hosted builds.") - - try withTemporaryDirectory { tempDir in - let packagePath = tempDir.appending(component: "dealer") - try sh("git", "clone", "https://github.com/apple/example-package-dealer", packagePath) - let build1Output = try sh(swiftBuild, "--package-path", packagePath).stdout - // Check the build log. - XCTAssertMatch(build1Output, .contains("Build complete")) - - // Verify that the app works. - let dealerOutput = try sh(AbsolutePath(validating: ".build/debug/dealer", relativeTo: packagePath), "10").stdout - XCTAssertEqual(dealerOutput.filter(\.isPlayingCardSuit).count, 10) - - // Verify that the 'git status' is clean after a build. - try localFileSystem.changeCurrentWorkingDirectory(to: packagePath) - let gitOutput = try sh("git", "status").stdout - XCTAssertMatch(gitOutput, .contains("nothing to commit, working tree clean")) - - // Verify that another 'swift build' does nothing. - let build2Output = try sh(swiftBuild, "--package-path", packagePath).stdout - XCTAssertMatch(build2Output, .contains("Build complete")) - XCTAssertNoMatch(build2Output, .contains("Compiling")) - } - } - - func testSwiftBuild() throws { - try withTemporaryDirectory { tempDir in - let packagePath = tempDir.appending(component: "tool") - try localFileSystem.createDirectory(packagePath) - try localFileSystem.writeFileContents( - packagePath.appending(component: "Package.swift"), - bytes: ByteString(encodingAsUTF8: """ - // swift-tools-version:4.2 - import PackageDescription - - let package = Package( - name: "tool", - targets: [ - .target(name: "tool", path: "./"), - ] - ) - """)) - try localFileSystem.writeFileContents( - packagePath.appending(component: "main.swift"), - bytes: ByteString(encodingAsUTF8: #"print("HI")"#)) - - // Check the build. - let buildOutput = try sh(swiftBuild, "--package-path", packagePath, "-v").stdout - XCTAssertMatch(buildOutput, .regex("swiftc.* -module-name tool")) - - // Verify that the tool exists and works. - let toolOutput = try sh(packagePath.appending(components: ".build", "debug", "tool")).stdout - XCTAssertEqual(toolOutput, "HI\n") - } - } - - func testSwiftCompiler() throws { - try withTemporaryDirectory { tempDir in - let helloSourcePath = tempDir.appending(component: "hello.swift") - try localFileSystem.writeFileContents( - helloSourcePath, - bytes: ByteString(encodingAsUTF8: #"print("hello")"#)) - let helloBinaryPath = tempDir.appending(component: "hello") - try sh(swiftc, helloSourcePath, "-o", helloBinaryPath) - - // Check the file exists. - XCTAssert(localFileSystem.exists(helloBinaryPath)) - - // Check the file runs. - let helloOutput = try sh(helloBinaryPath).stdout - XCTAssertEqual(helloOutput, "hello\n") - } - } - - func testSwiftPackageInitExec() throws { - #if swift(<5.5) - try XCTSkipIf(true, "skipping because host compiler doesn't support '-entry-point-function-name'") - #endif - - try withTemporaryDirectory { tempDir in - // Create a new package with an executable target. - let packagePath = tempDir.appending(component: "Project") - try localFileSystem.createDirectory(packagePath) - try sh(swiftPackage, "--package-path", packagePath, "init", "--type", "executable") - let buildOutput = try sh(swiftBuild, "--package-path", packagePath).stdout - - // Check the build log. - XCTAssertContents(buildOutput) { checker in - checker.check(.regex("Compiling .*Project.*")) - checker.check(.regex("Linking .*Project")) - checker.check(.contains("Build complete")) - } - - // Verify that the tool was built and works. - let toolOutput = try sh(packagePath.appending(components: ".build", "debug", "Project")).stdout - XCTAssertMatch(toolOutput.lowercased(), .contains("hello, world!")) - - // Check there were no compile errors or warnings. - XCTAssertNoMatch(buildOutput, .contains("error")) - XCTAssertNoMatch(buildOutput, .contains("warning")) - } - } - - func testSwiftPackageInitExecTests() throws { - #if swift(<5.5) - try XCTSkipIf(true, "skipping because host compiler doesn't support '-entry-point-function-name'") - #endif - - try XCTSkip("FIXME: swift-test invocations are timing out in Xcode and self-hosted CI") - - try withTemporaryDirectory { tempDir in - // Create a new package with an executable target. - let packagePath = tempDir.appending(component: "Project") - try localFileSystem.createDirectory(packagePath) - try sh(swiftPackage, "--package-path", packagePath, "init", "--type", "executable") - let testOutput = try sh(swiftTest, "--package-path", packagePath).stdout - - // Check the test log. - XCTAssertContents(testOutput) { checker in - checker.check(.regex("Compiling .*ProjectTests.*")) - checker.check("Test Suite 'All tests' passed") - checker.checkNext("Executed 1 test") - } - - // Check there were no compile errors or warnings. - XCTAssertNoMatch(testOutput, .contains("error")) - XCTAssertNoMatch(testOutput, .contains("warning")) - } - } - - func testSwiftPackageInitLib() throws { - try withTemporaryDirectory { tempDir in - // Create a new package with an executable target. - let packagePath = tempDir.appending(component: "Project") - try localFileSystem.createDirectory(packagePath) - try sh(swiftPackage, "--package-path", packagePath, "init", "--type", "library") - let buildOutput = try sh(swiftBuild, "--package-path", packagePath).stdout - - // Check the build log. - XCTAssertMatch(buildOutput, .regex("Compiling .*Project.*")) - XCTAssertMatch(buildOutput, .contains("Build complete")) - - // Check there were no compile errors or warnings. - XCTAssertNoMatch(buildOutput, .contains("error")) - XCTAssertNoMatch(buildOutput, .contains("warning")) - } - } - - func testSwiftPackageLibsTests() throws { - try XCTSkip("FIXME: swift-test invocations are timing out in Xcode and self-hosted CI") - - try withTemporaryDirectory { tempDir in - // Create a new package with an executable target. - let packagePath = tempDir.appending(component: "Project") - try localFileSystem.createDirectory(packagePath) - try sh(swiftPackage, "--package-path", packagePath, "init", "--type", "library") - let testOutput = try sh(swiftTest, "--package-path", packagePath).stdout - - // Check the test log. - XCTAssertContents(testOutput) { checker in - checker.check(.regex("Compiling .*ProjectTests.*")) - checker.check("Test Suite 'All tests' passed") - checker.checkNext("Executed 1 test") - } - - // Check there were no compile errors or warnings. - XCTAssertNoMatch(testOutput, .contains("error")) - XCTAssertNoMatch(testOutput, .contains("warning")) - } - } - - func testSwiftPackageWithSpaces() throws { - try withTemporaryDirectory { tempDir in - let packagePath = tempDir.appending(components: "more spaces", "special tool") - try localFileSystem.createDirectory(packagePath, recursive: true) - try localFileSystem.writeFileContents( - packagePath.appending(component: "Package.swift"), - bytes: ByteString(encodingAsUTF8: """ - // swift-tools-version:4.2 - import PackageDescription - - let package = Package( - name: "special tool", - targets: [ - .target(name: "special tool", path: "./"), - ] - ) - """)) - try localFileSystem.writeFileContents( - packagePath.appending(component: "main.swift"), - bytes: ByteString(encodingAsUTF8: #"foo()"#)) - try localFileSystem.writeFileContents( - packagePath.appending(component: "some file.swift"), - bytes: ByteString(encodingAsUTF8: #"func foo() { print("HI") }"#)) - - // Check the build. - let buildOutput = try sh(swiftBuild, "--package-path", packagePath, "-v").stdout - XCTAssertMatch(buildOutput, .regex(#"swiftc.* -module-name special_tool .* '@.*/more spaces/special tool/.build/[^/]+/debug/special_tool.build/sources'"#)) - XCTAssertMatch(buildOutput, .contains("Build complete")) - - // Verify that the tool exists and works. - let toolOutput = try sh(packagePath.appending(components: ".build", "debug", "special tool")).stdout - XCTAssertEqual(toolOutput, "HI\n") - } - } - - func testSwiftRun() throws { - #if swift(<5.5) - try XCTSkipIf(true, "skipping because host compiler doesn't support '-entry-point-function-name'") - #endif - - try withTemporaryDirectory { tempDir in - let packagePath = tempDir.appending(component: "secho") - try localFileSystem.createDirectory(packagePath) - try sh(swiftPackage, "--package-path", packagePath, "init", "--type", "executable") - // delete any files generated - for entry in try localFileSystem.getDirectoryContents(packagePath.appending(components: "Sources")) { - try localFileSystem.removeFileTree(packagePath.appending(components: "Sources", entry)) - } - try localFileSystem.writeFileContents( - packagePath.appending(components: "Sources", "secho.swift"), - bytes: ByteString(encodingAsUTF8: """ - import Foundation - print(CommandLine.arguments.dropFirst().joined(separator: " ")) - """)) - let (runOutput, runError) = try sh(swiftRun, "--package-path", packagePath, "secho", "1", #""two""#) - - // Check the run log. - XCTAssertContents(runError) { checker in - checker.check(.regex("Compiling .*secho.*")) - checker.check(.regex("Linking .*secho")) - checker.check(.contains("Build complete")) - } - XCTAssertEqual(runOutput, "1 \"two\"\n") - } - } - - func testSwiftTest() throws { - try XCTSkip("FIXME: swift-test invocations are timing out in Xcode and self-hosted CI") - - try withTemporaryDirectory { tempDir in - let packagePath = tempDir.appending(component: "swiftTest") - try localFileSystem.createDirectory(packagePath) - try sh(swiftPackage, "--package-path", packagePath, "init", "--type", "library") - try localFileSystem.writeFileContents( - packagePath.appending(components: "Tests", "swiftTestTests", "MyTests.swift"), - bytes: ByteString(encodingAsUTF8: """ - import XCTest - - final class MyTests: XCTestCase { - func testFoo() { - XCTAssertTrue(1 == 1) - } - func testBar() { - XCTAssertFalse(1 == 2) - } - func testBaz() { } - } - """)) - let testOutput = try sh(swiftTest, "--package-path", packagePath, "--filter", "MyTests.*", "--skip", "testBaz").stderr - - // Check the test log. - XCTAssertContents(testOutput) { checker in - checker.check(.contains("Test Suite 'MyTests' started")) - checker.check(.contains("Test Suite 'MyTests' passed")) - checker.check(.contains("Executed 2 tests, with 0 failures")) - } - } - } - - func testSwiftTestWithResources() throws { - try XCTSkip("FIXME: swift-test invocations are timing out in Xcode and self-hosted CI") - - try withTemporaryDirectory { tempDir in - let packagePath = tempDir.appending(component: "swiftTestResources") - try localFileSystem.createDirectory(packagePath) - try localFileSystem.writeFileContents( - packagePath.appending(component: "Package.swift"), - bytes: ByteString(encodingAsUTF8: """ - // swift-tools-version:5.3 - import PackageDescription - - let package = Package( - name: "AwesomeResources", - targets: [ - .target(name: "AwesomeResources", resources: [.copy("hello.txt")]), - .testTarget(name: "AwesomeResourcesTest", dependencies: ["AwesomeResources"], resources: [.copy("world.txt")]) - ] - ) - """) - ) - try localFileSystem.createDirectory(packagePath.appending(component: "Sources")) - try localFileSystem.createDirectory(packagePath.appending(components: "Sources", "AwesomeResources")) - try localFileSystem.writeFileContents( - packagePath.appending(components: "Sources", "AwesomeResources", "AwesomeResource.swift"), - bytes: ByteString(encodingAsUTF8: """ - import Foundation - - public struct AwesomeResource { - public init() {} - public let hello = try! String(contentsOf: Bundle.module.url(forResource: "hello", withExtension: "txt")!) - } - - """) - ) - - try localFileSystem.writeFileContents( - packagePath.appending(components: "Sources", "AwesomeResources", "hello.txt"), - bytes: ByteString(encodingAsUTF8: "hello") - ) - - try localFileSystem.createDirectory(packagePath.appending(component: "Tests")) - try localFileSystem.createDirectory(packagePath.appending(components: "Tests", "AwesomeResourcesTest")) - - try localFileSystem.writeFileContents( - packagePath.appending(components: "Tests", "AwesomeResourcesTest", "world.txt"), - bytes: ByteString(encodingAsUTF8: "world") - ) - - try localFileSystem.writeFileContents( - packagePath.appending(components: "Tests", "AwesomeResourcesTest", "MyTests.swift"), - bytes: ByteString(encodingAsUTF8: """ - import XCTest - import Foundation - import AwesomeResources - - final class MyTests: XCTestCase { - func testFoo() { - XCTAssertTrue(AwesomeResource().hello == "hello") - } - func testBar() { - let world = try! String(contentsOf: Bundle.module.url(forResource: "world", withExtension: "txt")!) - XCTAssertTrue(world == "world") - } - } - """)) - - let testOutput = try sh(swiftTest, "--package-path", packagePath, "--filter", "MyTests.*").stderr - - // Check the test log. - XCTAssertContents(testOutput) { checker in - checker.check(.contains("Test Suite 'MyTests' started")) - checker.check(.contains("Test Suite 'MyTests' passed")) - checker.check(.contains("Executed 2 tests, with 0 failures")) - } - } - } -} - -private extension Character { - var isPlayingCardSuit: Bool { - switch self { - case "♠︎", "♡", "♢", "♣︎": - return true - default: - return false - } - } -} diff --git a/IntegrationTests/Tests/IntegrationTests/Helpers.swift b/IntegrationTests/Tests/IntegrationTests/Helpers.swift deleted file mode 100644 index 0fb77e8eabe..00000000000 --- a/IntegrationTests/Tests/IntegrationTests/Helpers.swift +++ /dev/null @@ -1,359 +0,0 @@ -/* -This source file is part of the Swift.org open source project - -Copyright (c) 2014 - 2020 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 Swift project authors -*/ - -import Foundation -import XCTest -import TSCBasic -import TSCTestSupport - -import enum TSCUtility.Git - -let sdkRoot: AbsolutePath? = { - if let environmentPath = ProcessInfo.processInfo.environment["SDK_ROOT"] { - return try! AbsolutePath(validating: environmentPath) - } - - #if os(macOS) - let result = try! Process.popen(arguments: ["xcrun", "--sdk", "macosx", "--show-sdk-path"]) - let sdkRoot = try! AbsolutePath(validating: result.utf8Output().spm_chomp()) - return sdkRoot - #else - return nil - #endif -}() - -let toolchainPath: AbsolutePath = { - if let environmentPath = ProcessInfo.processInfo.environment["TOOLCHAIN_PATH"] { - return try! AbsolutePath(validating: environmentPath) - } - - #if os(macOS) - let swiftcPath = try! AbsolutePath(validating: sh("xcrun", "--find", "swift").stdout.spm_chomp()) - #else - let swiftcPath = try! AbsolutePath(validating: sh("which", "swift").stdout.spm_chomp()) - #endif - let toolchainPath = swiftcPath.parentDirectory.parentDirectory.parentDirectory - return toolchainPath -}() - -let clang: AbsolutePath = { - if let environmentPath = ProcessInfo.processInfo.environment["CLANG_PATH"] { - return try! AbsolutePath(validating: environmentPath) - } - - let clangPath = toolchainPath.appending(components: "usr", "bin", "clang") - return clangPath -}() - -let xcodebuild: AbsolutePath = { - #if os(macOS) - let xcodebuildPath = try! AbsolutePath(validating: sh("xcrun", "--find", "xcodebuild").stdout.spm_chomp()) - return xcodebuildPath - #else - fatalError("should not be used on other platforms than macOS") - #endif -}() - -let swift: AbsolutePath = { - if let environmentPath = ProcessInfo.processInfo.environment["SWIFT_PATH"] { - return try! AbsolutePath(validating: environmentPath) - } - - let swiftPath = toolchainPath.appending(components: "usr", "bin", "swift") - return swiftPath -}() - -let swiftc: AbsolutePath = { - if let environmentPath = ProcessInfo.processInfo.environment["SWIFTC_PATH"] { - return try! AbsolutePath(validating: environmentPath) - } - - let swiftcPath = toolchainPath.appending(components: "usr", "bin", "swiftc") - return swiftcPath -}() - -let lldb: AbsolutePath = { - if let environmentPath = ProcessInfo.processInfo.environment["LLDB_PATH"] { - return try! AbsolutePath(validating: environmentPath) - } - - // We check if it exists because lldb doesn't exist in Xcode's default toolchain. - let toolchainLLDBPath = toolchainPath.appending(components: "usr", "bin", "lldb") - if localFileSystem.exists(toolchainLLDBPath) { - return toolchainLLDBPath - } - - #if os(macOS) - let lldbPath = try! AbsolutePath(validating: sh("xcrun", "--find", "lldb").stdout.spm_chomp()) - return lldbPath - #else - fatalError("LLDB_PATH environment variable required") - #endif -}() - -let swiftpmBinaryDirectory: AbsolutePath = { - if let environmentPath = ProcessInfo.processInfo.environment["SWIFTPM_BIN_DIR"] { - return try! AbsolutePath(validating: environmentPath) - } - - return swift.parentDirectory -}() - -let swiftBuild: AbsolutePath = { - return swiftpmBinaryDirectory.appending(component: "swift-build") -}() - -let swiftPackage: AbsolutePath = { - return swiftpmBinaryDirectory.appending(component: "swift-package") -}() - -let swiftTest: AbsolutePath = { - return swiftpmBinaryDirectory.appending(component: "swift-test") -}() - -let swiftRun: AbsolutePath = { - return swiftpmBinaryDirectory.appending(component: "swift-run") -}() - -let isSelfHosted: Bool = { - return ProcessInfo.processInfo.environment["SWIFTCI_IS_SELF_HOSTED"] != nil -}() - -@discardableResult -func sh( - _ arguments: CustomStringConvertible..., - env: [String: String] = [:], - file: StaticString = #file, - line: UInt = #line -) throws -> (stdout: String, stderr: String) { - let result = try _sh(arguments, env: env, file: file, line: line) - let stdout = try result.utf8Output() - let stderr = try result.utf8stderrOutput() - - if result.exitStatus != .terminated(code: 0) { - XCTFail("Command failed with exit code: \(result.exitStatus) - \(result.integrationTests_debugDescription)", file: file, line: line) - } - - return (stdout, stderr) -} - -@discardableResult -func shFails( - _ arguments: CustomStringConvertible..., - env: [String: String] = [:], - file: StaticString = #file, - line: UInt = #line -) throws -> (stdout: String, stderr: String) { - let result = try _sh(arguments, env: env, file: file, line: line) - let stdout = try result.utf8Output() - let stderr = try result.utf8stderrOutput() - - if result.exitStatus == .terminated(code: 0) { - XCTFail("Command unexpectedly succeeded with exit code: \(result.exitStatus) - \(result.integrationTests_debugDescription)", file: file, line: line) - } - - return (stdout, stderr) -} - -@discardableResult -func _sh( - _ arguments: [CustomStringConvertible], - env: [String: String] = [:], - file: StaticString = #file, - line: UInt = #line -) throws -> ProcessResult { - var environment = ProcessInfo.processInfo.environment - - if let sdkRoot = sdkRoot { - environment["SDKROOT"] = sdkRoot.pathString - } - - environment.merge(env, uniquingKeysWith: { $1 }) - - let result = try Process.popen(arguments: arguments.map { $0.description }, environment: environment) - return result -} - -/// Test-helper function that runs a block of code on a copy of a test fixture -/// package. The copy is made into a temporary directory, and the block is -/// given a path to that directory. The block is permitted to modify the copy. -/// The temporary copy is deleted after the block returns. The fixture name may -/// contain `/` characters, which are treated as path separators, exactly as if -/// the name were a relative path. -func fixture( - name: String, - file: StaticString = #file, - line: UInt = #line, - body: (AbsolutePath) throws -> Void -) { - do { - // Make a suitable test directory name from the fixture subpath. - let fixtureSubpath = try RelativePath(validating: name) - let copyName = fixtureSubpath.components.joined(separator: "_") - - // Create a temporary directory for the duration of the block. - try withTemporaryDirectory(prefix: copyName) { tmpDirPath in - - defer { - // Unblock and remove the tmp dir on deinit. - try? localFileSystem.chmod(.userWritable, path: tmpDirPath, options: [.recursive]) - try? localFileSystem.removeFileTree(tmpDirPath) - } - - // Construct the expected path of the fixture. - // FIXME: This seems quite hacky; we should provide some control over where fixtures are found. - let fixtureDir = try AbsolutePath( - validating: "../../../Fixtures/\(name)", - relativeTo: AbsolutePath(validating: #file) - ) - - // Check that the fixture is really there. - guard localFileSystem.isDirectory(fixtureDir) else { - XCTFail("No such fixture: \(fixtureDir)", file: file, line: line) - return - } - - // The fixture contains either a checkout or just a Git directory. - if localFileSystem.isFile(fixtureDir.appending(component: "Package.swift")) { - // It's a single package, so copy the whole directory as-is. - let dstDir = tmpDirPath.appending(component: copyName) -#if os(Windows) - try localFileSystem.copy(from: fixtureDir, to: dstDir) -#else - try systemQuietly("cp", "-R", "-H", fixtureDir.pathString, dstDir.pathString) -#endif - - // Invoke the block, passing it the path of the copied fixture. - try body(dstDir) - } else { - // Copy each of the package directories and construct a git repo in it. - for fileName in try! localFileSystem.getDirectoryContents(fixtureDir).sorted() { - let srcDir = fixtureDir.appending(component: fileName) - guard localFileSystem.isDirectory(srcDir) else { continue } - let dstDir = tmpDirPath.appending(component: fileName) -#if os(Windows) - try localFileSystem.copy(from: srcDir, to: dstDir) -#else - try systemQuietly("cp", "-R", "-H", srcDir.pathString, dstDir.pathString) -#endif - initGitRepo(dstDir, tag: "1.2.3", addFile: false) - } - - // Invoke the block, passing it the path of the copied fixture. - try body(tmpDirPath) - } - } - } catch { - XCTFail("\(error)", file: file, line: line) - } -} - -/// Test-helper function that creates a new Git repository in a directory. The new repository will contain -/// exactly one empty file unless `addFile` is `false`, and if a tag name is provided, a tag with that name will be -/// created. -func initGitRepo( - _ dir: AbsolutePath, - tag: String? = nil, - addFile: Bool = true, - file: StaticString = #file, - line: UInt = #line -) { - initGitRepo(dir, tags: tag.flatMap({ [$0] }) ?? [], addFile: addFile, file: file, line: line) -} - -func initGitRepo( - _ dir: AbsolutePath, - tags: [String], - addFile: Bool = true, - file: StaticString = #file, - line: UInt = #line -) { - do { - if addFile { - let file = dir.appending(component: "file.swift") - try localFileSystem.writeFileContents(file, bytes: "") - } - - try systemQuietly([Git.tool, "-C", dir.pathString, "init"]) - try systemQuietly([Git.tool, "-C", dir.pathString, "config", "user.email", "example@example.com"]) - try systemQuietly([Git.tool, "-C", dir.pathString, "config", "user.name", "Example Example"]) - try systemQuietly([Git.tool, "-C", dir.pathString, "config", "commit.gpgsign", "false"]) - try systemQuietly([Git.tool, "-C", dir.pathString, "add", "."]) - try systemQuietly([Git.tool, "-C", dir.pathString, "commit", "-m", "Add some files."]) - - for tag in tags { - try systemQuietly([Git.tool, "-C", dir.pathString, "tag", tag]) - } - } catch { - XCTFail("\(error)", file: file, line: line) - } -} - -func binaryTargetsFixture(_ closure: (AbsolutePath) throws -> Void) throws { - fixture(name: "BinaryTargets") { fixturePath in - let inputsPath = fixturePath.appending(component: "Inputs") - let packagePath = fixturePath.appending(component: "TestBinary") - - // Generating StaticLibrary.xcframework. - try withTemporaryDirectory { tmpDir in - let subpath = inputsPath.appending(component: "StaticLibrary") - let sourcePath = subpath.appending(component: "StaticLibrary.m") - let headersPath = subpath.appending(component: "include") - let libraryPath = tmpDir.appending(component: "libStaticLibrary.a") - try sh(clang, "-c", sourcePath, "-I", headersPath, "-fobjc-arc", "-fmodules", "-o", libraryPath) - let xcframeworkPath = packagePath.appending(component: "StaticLibrary.xcframework") - try sh(xcodebuild, "-create-xcframework", "-library", libraryPath, "-headers", headersPath, "-output", xcframeworkPath) - } - - // Generating DynamicLibrary.xcframework. - try withTemporaryDirectory { tmpDir in - let subpath = inputsPath.appending(component: "DynamicLibrary") - let sourcePath = subpath.appending(component: "DynamicLibrary.m") - let headersPath = subpath.appending(component: "include") - let libraryPath = tmpDir.appending(component: "libDynamicLibrary.dylib") - try sh(clang, sourcePath, "-I", headersPath, "-fobjc-arc", "-fmodules", "-dynamiclib", "-o", libraryPath) - let xcframeworkPath = packagePath.appending(component: "DynamicLibrary.xcframework") - try sh(xcodebuild, "-create-xcframework", "-library", libraryPath, "-headers", headersPath, "-output", xcframeworkPath) - } - - // Generating SwiftFramework.xcframework. - try withTemporaryDirectory { tmpDir in - let subpath = inputsPath.appending(component: "SwiftFramework") - let projectPath = subpath.appending(component: "SwiftFramework.xcodeproj") - try sh(xcodebuild, "-project", projectPath, "-scheme", "SwiftFramework", "-derivedDataPath", tmpDir, "COMPILER_INDEX_STORE_ENABLE=NO") - let frameworkPath = try AbsolutePath( - validating: "Build/Products/Debug/SwiftFramework.framework", - relativeTo: tmpDir - ) - let xcframeworkPath = packagePath.appending(component: "SwiftFramework.xcframework") - try sh(xcodebuild, "-create-xcframework", "-framework", frameworkPath, "-output", xcframeworkPath) - } - - try closure(packagePath) - } -} - -func XCTSkip(_ message: String? = nil) throws { - throw XCTSkip(message) -} - -extension ProcessResult { - var integrationTests_debugDescription: String { - return """ - command: \(arguments.map { $0.description }.joined(separator: " ")) - - stdout: - \((try? utf8Output()) ?? "") - - stderr: - \((try? utf8stderrOutput()) ?? "") - """ - } -} diff --git a/IntegrationTests/Tests/IntegrationTests/StringChecker.swift b/IntegrationTests/Tests/IntegrationTests/StringChecker.swift deleted file mode 100644 index 39e7c2bf411..00000000000 --- a/IntegrationTests/Tests/IntegrationTests/StringChecker.swift +++ /dev/null @@ -1,62 +0,0 @@ -/* -This source file is part of the Swift.org open source project - -Copyright (c) 2014 - 2020 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 Swift project authors -*/ - -import Foundation -import TSCTestSupport -import XCTest - -class StringChecker { - private let string: String - private let lines: [Substring] - private var currentLineIndex: Int - private var currentLine: Substring { lines[currentLineIndex] } - - init(string: String) { - self.string = string - self.lines = string.split(separator: "\n") - self.currentLineIndex = 0 - } - - func check(_ pattern: StringPattern, file: StaticString = #file, line: UInt = #line) { - while let line = nextLine() { - if pattern ~= line { - return - } - } - - XCTFail(string, file: file, line: line) - } - - func checkNext(_ pattern: StringPattern, file: StaticString = #file, line: UInt = #line) { - if let line = nextLine(), pattern ~= line { - return - } - - XCTFail(string, file: file, line: line) - } - - private func nextLine() -> String? { - guard currentLineIndex < lines.count else { - return nil - } - - let currentLine = lines[currentLineIndex] - currentLineIndex += 1 - return String(currentLine) - } -} - -func XCTAssertContents( - _ string: String, - _ check: (StringChecker) -> Void -) { - let checker = StringChecker(string: string) - check(checker) -} diff --git a/IntegrationTests/Tests/IntegrationTests/SwiftPMTests.swift b/IntegrationTests/Tests/IntegrationTests/SwiftPMTests.swift deleted file mode 100644 index d9fcdccc1bf..00000000000 --- a/IntegrationTests/Tests/IntegrationTests/SwiftPMTests.swift +++ /dev/null @@ -1,94 +0,0 @@ -/* - This source file is part of the Swift.org open source project - - Copyright (c) 2014 - 2020 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 Swift project authors - */ - -import XCTest -import TSCBasic -import TSCTestSupport - -final class SwiftPMTests: XCTestCase { - func testBinaryTargets() throws { - try XCTSkip("FIXME: ld: warning: dylib (/../BinaryTargets.6YVYK4/TestBinary/.build/x86_64-apple-macosx/debug/SwiftFramework.framework/SwiftFramework) was built for newer macOS version (10.15) than being linked (10.10)") - -#if !os(macOS) - try XCTSkip("Test requires macOS") -#endif - - try binaryTargetsFixture { fixturePath in - do { - let (stdout, stderr) = try sh(swiftRun, "--package-path", fixturePath, "exe") - XCTAssertNoMatch(stderr, .contains("warning: ")) - XCTAssertEqual(stdout, """ - SwiftFramework() - Library(framework: SwiftFramework.SwiftFramework()) - - """) - } - - do { - let (stdout, stderr) = try sh(swiftRun, "--package-path", fixturePath, "cexe") - XCTAssertNoMatch(stderr, .contains("warning: ")) - XCTAssertMatch(stdout, .contains("=6.1. These targets opt in to using `swift6CompatibleExperimentalFeatures`. +#if swift(>=6.1) +let swift6CompatibleExperimentalFeatures = commonExperimentalFeatures +#else +let swift6CompatibleExperimentalFeatures: [SwiftSetting] = [] +#endif + /** SwiftPMDataModel is the subset of SwiftPM product that includes just its data model. -This allows some clients (such as IDEs) that use SwiftPM's data model but not its build system -to not have to depend on SwiftDriver, SwiftLLBuild, etc. We should probably have better names here, -though that could break some clients. -*/ + This allows some clients (such as IDEs) that use SwiftPM's data model but not its build system + to not have to depend on SwiftDriver, SwiftLLBuild, etc. We should probably have better names here, + though that could break some clients. + */ let swiftPMDataModelProduct = ( name: "SwiftPMDataModel", targets: [ @@ -51,7 +72,7 @@ let swiftPMDataModelProduct = ( command line tools, while `libSwiftPMDataModel` includes only the data model. NOTE: This API is *unstable* and may change at any time. -*/ + */ let swiftPMProduct = ( name: "SwiftPM", targets: swiftPMDataModelProduct.targets + [ @@ -63,36 +84,71 @@ let swiftPMProduct = ( ) #if os(Windows) +let includeDynamicLibrary: Bool = false let systemSQLitePkgConfig: String? = nil #else -let systemSQLitePkgConfig: String? = "sqlite3" +let includeDynamicLibrary: Bool = true +var systemSQLitePkgConfig: String? = "sqlite3" +if ProcessInfo.processInfo.environment["SWIFTCI_INSTALL_RPATH_OS"] == "android" { + systemSQLitePkgConfig = nil +} #endif /** An array of products which have two versions listed: one dynamically linked, the other with the -automatic linking type with `-auto` suffix appended to product's name. -*/ + automatic linking type with `-auto` suffix appended to product's name. + */ let autoProducts = [swiftPMProduct, swiftPMDataModelProduct] +let shouldUseSwiftBuildFramework = (ProcessInfo.processInfo.environment["SWIFTPM_SWBUILD_FRAMEWORK"] != nil) + +let swiftDriverDeps: [Target.Dependency] +let swiftTSCBasicsDeps: [Target.Dependency] +let swiftToolsCoreSupportAutoDeps: [Target.Dependency] +let swiftTSCTestSupportDeps: [Target.Dependency] + +if shouldUseSwiftBuildFramework { + swiftDriverDeps = [] + swiftTSCBasicsDeps = [] + swiftToolsCoreSupportAutoDeps = [] + swiftTSCTestSupportDeps = [] +} else { + swiftDriverDeps = [ + .product(name: "SwiftDriver", package: "swift-driver") + ] + swiftTSCBasicsDeps = [ + .product(name: "TSCBasic", package: "swift-tools-support-core"), + ] + swiftToolsCoreSupportAutoDeps = [ + .product(name: "SwiftToolsSupport-auto", package: "swift-tools-support-core") + ] + swiftTSCTestSupportDeps = [ + .product(name: "TSCTestSupport", package: "swift-tools-support-core"), + ] +} let package = Package( name: "SwiftPM", platforms: [ - .macOS(.v12), - .iOS(.v15) + .macOS(.v14), + .iOS(.v17), + .macCatalyst(.v17), ], products: - autoProducts.flatMap { - [ + autoProducts.flatMap { + (includeDynamicLibrary ? [ .library( name: $0.name, type: .dynamic, targets: $0.targets ), + ] : []) + + + [ .library( name: "\($0.name)-auto", targets: $0.targets - ) - ] - } + [ + ), + ] + } + [ .library( name: "XCBuildSupport", targets: ["XCBuildSupport"] @@ -102,6 +158,12 @@ let package = Package( type: .dynamic, targets: ["PackageDescription", "CompilerPluginSupport"] ), + .library( + name: "AppleProductTypes", + type: .dynamic, + targets: ["AppleProductTypes"] + ), + .library( name: "PackagePlugin", type: .dynamic, @@ -122,38 +184,35 @@ let package = Package( ), ], targets: [ - // The `PackageDescription` target provides the API that is available + // The `AppleProductTypes` target provides additional product types // to `Package.swift` manifests. Here we build a debug version of the // library; the bootstrap scripts build the deployable version. .target( - name: "PackageDescription", - exclude: ["CMakeLists.txt"], - swiftSettings: [ - .define("USE_IMPL_ONLY_IMPORTS"), - .unsafeFlags(["-package-description-version", "999.0"]), - .unsafeFlags(["-enable-library-evolution"]), - ], - linkerSettings: packageLibraryLinkSettings - ), - - // The `PackagePlugin` target provides the API that is available to - // plugin scripts. Here we build a debug version of the library; the - // bootstrap scripts build the deployable version. - .target( - name: "PackagePlugin", - exclude: ["CMakeLists.txt"], - swiftSettings: [ + name: "AppleProductTypes", + // Note: We use `-module-link-name` so clients link against the + // AppleProductTypes library when they import it without further + // messing with the manifest loader. + dependencies: ["PackageDescription"], + swiftSettings: commonExperimentalFeatures + [ .unsafeFlags(["-package-description-version", "999.0"]), - .unsafeFlags(["-enable-library-evolution"]), - ], - linkerSettings: packageLibraryLinkSettings - ), + .unsafeFlags(["-enable-library-evolution"], .when(platforms: [.macOS])), + .unsafeFlags(["-Xfrontend", "-module-link-name", "-Xfrontend", "AppleProductTypes"]) + ]), .target( name: "SourceKitLSPAPI", dependencies: [ + "Basics", "Build", - "SPMBuildCore" + "PackageGraph", + "PackageLoading", + "PackageModel", + "SPMBuildCore", + ], + exclude: ["CMakeLists.txt"], + swiftSettings: commonExperimentalFeatures + [ + .enableExperimentalFeature("AccessLevelOnImport"), + .unsafeFlags(["-static"]), ] ), @@ -161,23 +220,46 @@ let package = Package( .systemLibrary(name: "SPMSQLite3", pkgConfig: systemSQLitePkgConfig), + .target( + name: "_AsyncFileSystem", + dependencies: [ + .product(name: "SystemPackage", package: "swift-system"), + ], + exclude: ["CMakeLists.txt"], + swiftSettings: commonExperimentalFeatures + [ + .enableExperimentalFeature("StrictConcurrency"), + .enableExperimentalFeature("AccessLevelOnImport"), + .enableExperimentalFeature("InternalImportsByDefault"), + .unsafeFlags(["-static"]), + ] + ), + .target( name: "Basics", dependencies: [ - "SPMSQLite3", + "_AsyncFileSystem", + .target(name: "SPMSQLite3", condition: .when(platforms: [.macOS, .iOS, .tvOS, .watchOS, .visionOS, .macCatalyst, .linux, .openbsd, .custom("freebsd")])), + .product(name: "SwiftToolchainCSQLite", package: "swift-toolchain-sqlite", condition: .when(platforms: [.windows, .android])), .product(name: "DequeModule", package: "swift-collections"), .product(name: "OrderedCollections", package: "swift-collections"), - .product(name: "SwiftToolsSupport-auto", package: "swift-tools-support-core"), .product(name: "SystemPackage", package: "swift-system"), - ], - exclude: ["CMakeLists.txt", "Vendor/README.md"] + ] + swiftToolsCoreSupportAutoDeps, + exclude: ["CMakeLists.txt", "Vendor/README.md"], + swiftSettings: swift6CompatibleExperimentalFeatures + [ + .enableExperimentalFeature("StrictConcurrency"), + .enableExperimentalFeature("AccessLevelOnImport"), + .unsafeFlags(["-static"]), + ] ), .target( /** The llbuild manifest model */ name: "LLBuildManifest", dependencies: ["Basics"], - exclude: ["CMakeLists.txt"] + exclude: ["CMakeLists.txt"], + swiftSettings: commonExperimentalFeatures + [ + .unsafeFlags(["-static"]), + ] ), .target( @@ -190,7 +272,10 @@ let package = Package( "PackageModel", "PackageSigning", ], - exclude: ["CMakeLists.txt"] + exclude: ["CMakeLists.txt"], + swiftSettings: commonExperimentalFeatures + [ + .unsafeFlags(["-static"]), + ] ), .target( @@ -198,16 +283,48 @@ let package = Package( name: "SourceControl", dependencies: [ "Basics", - "PackageModel" + "PackageModel", ], - exclude: ["CMakeLists.txt"] + exclude: ["CMakeLists.txt"], + swiftSettings: commonExperimentalFeatures + [ + .unsafeFlags(["-static"]), + ] ), .target( /** Shim for llbuild library */ name: "SPMLLBuild", dependencies: ["Basics"], - exclude: ["CMakeLists.txt"] + exclude: ["CMakeLists.txt"], + swiftSettings: commonExperimentalFeatures + [ + .unsafeFlags(["-static"]), + ] + ), + + .target( + /** API for deserializing diagnostics and applying fix-its */ + name: "SwiftFixIt", + dependencies: [ + "Basics", + ] + swiftTSCBasicsDeps + swiftSyntaxDependencies( + ["SwiftDiagnostics", "SwiftIDEUtils", "SwiftParser", "SwiftSyntax"] + ), + exclude: ["CMakeLists.txt"], + swiftSettings: commonExperimentalFeatures + [ + .unsafeFlags(["-static"]), + ] + ), + + .target( + /** API for inspecting symbols defined in binaries */ + name: "BinarySymbols", + dependencies: [ + "Basics", + ] + swiftTSCBasicsDeps, + exclude: ["CMakeLists.txt"], + swiftSettings: commonExperimentalFeatures + [ + .unsafeFlags(["-static"]), + ] ), // MARK: Project Model @@ -216,7 +333,10 @@ let package = Package( /** Primitive Package model objects */ name: "PackageModel", dependencies: ["Basics"], - exclude: ["CMakeLists.txt", "README.md"] + exclude: ["CMakeLists.txt", "README.md"], + swiftSettings: swift6CompatibleExperimentalFeatures + [ + .unsafeFlags(["-static"]), + ] ), .target( @@ -224,9 +344,13 @@ let package = Package( name: "PackageLoading", dependencies: [ "Basics", - "PackageModel" + "PackageModel", + "SourceControl", ], - exclude: ["CMakeLists.txt", "README.md"] + exclude: ["CMakeLists.txt", "README.md"], + swiftSettings: commonExperimentalFeatures + [ + .unsafeFlags(["-static"]), + ] ), // MARK: Package Dependency Resolution @@ -237,9 +361,13 @@ let package = Package( dependencies: [ "Basics", "PackageLoading", - "PackageModel" + "PackageModel", + .product(name: "OrderedCollections", package: "swift-collections"), ], - exclude: ["CMakeLists.txt", "README.md"] + exclude: ["CMakeLists.txt", "README.md"], + swiftSettings: commonExperimentalFeatures + [ + .unsafeFlags(["-static"]), + ] ), // MARK: Package Collections @@ -249,7 +377,11 @@ let package = Package( name: "PackageCollectionsModel", dependencies: [], exclude: [ - "Formats/v1.md" + "Formats/v1.md", + "CMakeLists.txt", + ], + swiftSettings: commonExperimentalFeatures + [ + .unsafeFlags(["-static"]), ] ), @@ -262,6 +394,10 @@ let package = Package( "PackageCollectionsSigning", "PackageModel", "SourceControl", + ], + exclude: ["CMakeLists.txt"], + swiftSettings: swift6CompatibleExperimentalFeatures + [ + .unsafeFlags(["-static"]), ] ), @@ -272,6 +408,10 @@ let package = Package( .product(name: "X509", package: "swift-certificates"), "Basics", "PackageCollectionsModel", + ], + exclude: ["CMakeLists.txt"], + swiftSettings: commonExperimentalFeatures + [ + .unsafeFlags(["-static"]), ] ), @@ -281,9 +421,12 @@ let package = Package( "Basics", "PackageModel", ], - exclude: ["CMakeLists.txt"] + exclude: ["CMakeLists.txt"], + swiftSettings: commonExperimentalFeatures + [ + .unsafeFlags(["-static"]), + ] ), - + .target( name: "PackageSigning", dependencies: [ @@ -292,7 +435,17 @@ let package = Package( "Basics", "PackageModel", ], - exclude: ["CMakeLists.txt"] + exclude: ["CMakeLists.txt"], + swiftSettings: commonExperimentalFeatures + [ + .unsafeFlags(["-static"]), + ] + ), + + // MARK: Documentation + + .target( + name: "PackageManagerDocs", + exclude: ["README.md"], ), // MARK: Package Manager Functionality @@ -302,9 +455,13 @@ let package = Package( name: "SPMBuildCore", dependencies: [ "Basics", - "PackageGraph" + "PackageGraph", + .product(name: "OrderedCollections", package: "swift-collections"), ], - exclude: ["CMakeLists.txt"] + exclude: ["CMakeLists.txt"], + swiftSettings: commonExperimentalFeatures + [ + .unsafeFlags(["-static"]), + ] ), .target( /** Builds Modules and Products */ @@ -315,25 +472,46 @@ let package = Package( "PackageGraph", "SPMBuildCore", "SPMLLBuild", - .product(name: "SwiftDriver", package: "swift-driver"), + .product(name: "OrderedCollections", package: "swift-collections"), "DriverSupport", - ], - exclude: ["CMakeLists.txt"] + ] + swiftDriverDeps, + exclude: ["CMakeLists.txt"], + swiftSettings: commonExperimentalFeatures + [ + .unsafeFlags(["-static"]), + ] ), .target( name: "DriverSupport", dependencies: [ "Basics", "PackageModel", - .product(name: "SwiftDriver", package: "swift-driver"), - ], - exclude: ["CMakeLists.txt"] + ] + swiftDriverDeps, + exclude: ["CMakeLists.txt"], + swiftSettings: commonExperimentalFeatures + [ + .unsafeFlags(["-static"]), + ] ), .target( /** Support for building using Xcode's build system */ name: "XCBuildSupport", - dependencies: ["SPMBuildCore", "PackageGraph"], - exclude: ["CMakeLists.txt", "CODEOWNERS"] + dependencies: [ + "SPMBuildCore", + "PackageGraph", + .product(name: "OrderedCollections", package: "swift-collections"), + ], + exclude: ["CMakeLists.txt"], + swiftSettings: commonExperimentalFeatures + [ + .unsafeFlags(["-static"]), + ] + ), + .target( + name: "SwiftBuildSupport", + dependencies: [ + "SPMBuildCore", + "PackageGraph", + ], + exclude: ["CMakeLists.txt", "README.md"], + swiftSettings: commonExperimentalFeatures ), .target( /** High level functionality */ @@ -347,8 +525,12 @@ let package = Package( "PackageSigning", "SourceControl", "SPMBuildCore", + .product(name: "OrderedCollections", package: "swift-collections"), ], - exclude: ["CMakeLists.txt"] + exclude: ["CMakeLists.txt"], + swiftSettings: commonExperimentalFeatures + [ + .unsafeFlags(["-static"]), + ] ), .target( // ** High level interface for package discovery */ @@ -359,6 +541,9 @@ let package = Package( "PackageModel", "PackageRegistry", "PackageSigning", + ], + swiftSettings: commonExperimentalFeatures + [ + .unsafeFlags(["-static"]), ] ), @@ -376,8 +561,12 @@ let package = Package( "PackageGraph", "Workspace", "XCBuildSupport", + "SwiftBuildSupport", ], - exclude: ["CMakeLists.txt"] + exclude: ["CMakeLists.txt"], + swiftSettings: commonExperimentalFeatures + [ + .unsafeFlags(["-static"]), + ] ), .target( @@ -385,20 +574,27 @@ let package = Package( name: "Commands", dependencies: [ .product(name: "ArgumentParser", package: "swift-argument-parser"), + .product(name: "OrderedCollections", package: "swift-collections"), "Basics", + "BinarySymbols", "Build", "CoreCommands", "PackageGraph", "SourceControl", "Workspace", "XCBuildSupport", - ], - exclude: ["CMakeLists.txt", "README.md"] + "SwiftBuildSupport", + "SwiftFixIt", + ] + swiftSyntaxDependencies(["SwiftIDEUtils", "SwiftRefactor"]), + exclude: ["CMakeLists.txt", "README.md"], + swiftSettings: swift6CompatibleExperimentalFeatures + [ + .unsafeFlags(["-static"]), + ] ), .target( /** Interacts with Swift SDKs used for cross-compilation */ - name: "SwiftSDKTool", + name: "SwiftSDKCommand", dependencies: [ .product(name: "ArgumentParser", package: "swift-argument-parser"), "Basics", @@ -406,12 +602,15 @@ let package = Package( "SPMBuildCore", "PackageModel", ], - exclude: ["CMakeLists.txt", "README.md"] + exclude: ["CMakeLists.txt", "README.md"], + swiftSettings: commonExperimentalFeatures + [ + .unsafeFlags(["-static"]), + ] ), .target( /** Interacts with package collections */ - name: "PackageCollectionsTool", + name: "PackageCollectionsCommand", dependencies: [ .product(name: "ArgumentParser", package: "swift-argument-parser"), "Basics", @@ -419,12 +618,15 @@ let package = Package( "CoreCommands", "PackageCollections", "PackageModel", + ], + swiftSettings: commonExperimentalFeatures + [ + .unsafeFlags(["-static"]), ] ), .target( /** Interact with package registry */ - name: "PackageRegistryTool", + name: "PackageRegistryCommand", dependencies: [ .product(name: "ArgumentParser", package: "swift-argument-parser"), "Basics", @@ -438,6 +640,24 @@ let package = Package( "SourceControl", "SPMBuildCore", "Workspace", + ], + exclude: ["CMakeLists.txt"], + swiftSettings: commonExperimentalFeatures + [ + .unsafeFlags(["-static"]), + ] + ), + + .target( + name: "QueryEngine", + dependencies: [ + "_AsyncFileSystem", + "Basics", + .product(name: "Crypto", package: "swift-crypto"), + ], + exclude: ["CMakeLists.txt"], + swiftSettings: [ + .enableExperimentalFeature("StrictConcurrency=complete"), + .unsafeFlags(["-static"]), ] ), @@ -458,19 +678,27 @@ let package = Package( name: "swift-bootstrap", dependencies: [ .product(name: "ArgumentParser", package: "swift-argument-parser"), + .product(name: "OrderedCollections", package: "swift-collections"), "Basics", "Build", "PackageGraph", "PackageLoading", "PackageModel", "XCBuildSupport", + "SwiftBuildSupport", ], exclude: ["CMakeLists.txt"] ), .executableTarget( /** Interacts with Swift SDKs used for cross-compilation */ + name: "swift-sdk", + dependencies: ["Commands", "SwiftSDKCommand"], + exclude: ["CMakeLists.txt"] + ), + .executableTarget( + /** Deprecated command superseded by `swift-sdk` */ name: "swift-experimental-sdk", - dependencies: ["Commands", "SwiftSDKTool"], + dependencies: ["Commands", "SwiftSDKCommand"], exclude: ["CMakeLists.txt"] ), .executableTarget( @@ -488,24 +716,65 @@ let package = Package( .executableTarget( /** Interacts with package collections */ name: "swift-package-collection", - dependencies: ["Commands", "PackageCollectionsTool"] + dependencies: ["Commands", "PackageCollectionsCommand"] ), .executableTarget( - /** Multi-tool entry point for SwiftPM. */ + /** Multi-command entry point for SwiftPM. */ name: "swift-package-manager", dependencies: [ "Basics", "Commands", - "SwiftSDKTool", - "PackageCollectionsTool", - "PackageRegistryTool" + "SwiftSDKCommand", + "PackageCollectionsCommand", + "PackageRegistryCommand", ], linkerSettings: swiftpmLinkSettings ), .executableTarget( /** Interact with package registry */ name: "swift-package-registry", - dependencies: ["Commands", "PackageRegistryTool"] + dependencies: ["Commands", "PackageRegistryCommand"] + ), + .executableTarget( + /** Utility to produce the artifacts for prebuilts */ + name: "swift-build-prebuilts", + dependencies: [ + .product(name: "ArgumentParser", package: "swift-argument-parser"), + "Basics", + "Workspace", + ], + exclude: [ + "build.sh" + ] + ), + + // The `PackageDescription` target provides the API that is available + // to `Package.swift` manifests. Here we build a debug version of the + // library; the bootstrap scripts build the deployable version. + .target( + name: "PackageDescription", + path: "Sources/Runtimes/PackageDescription", + exclude: ["CMakeLists.txt"], + swiftSettings: commonExperimentalFeatures + [ + .define("USE_IMPL_ONLY_IMPORTS"), + .unsafeFlags(["-package-description-version", "999.0"]), + .unsafeFlags(["-enable-library-evolution"]), + ], + linkerSettings: packageLibraryLinkSettings + ), + + // The `PackagePlugin` target provides the API that is available to + // plugin scripts. Here we build a debug version of the library; the + // bootstrap scripts build the deployable version. + .target( + name: "PackagePlugin", + path: "Sources/Runtimes/PackagePlugin", + exclude: ["CMakeLists.txt"], + swiftSettings: commonExperimentalFeatures + [ + .unsafeFlags(["-package-description-version", "999.0"]), + .unsafeFlags(["-enable-library-evolution"]), + ], + linkerSettings: packageLibraryLinkSettings ), // MARK: Support for Swift macros, should eventually move to a plugin-based solution @@ -513,8 +782,9 @@ let package = Package( .target( name: "CompilerPluginSupport", dependencies: ["PackageDescription"], + path: "Sources/Runtimes/CompilerPluginSupport", exclude: ["CMakeLists.txt"], - swiftSettings: [ + swiftSettings: commonExperimentalFeatures + [ .unsafeFlags(["-package-description-version", "999.0"]), .unsafeFlags(["-enable-library-evolution"]), ] @@ -523,59 +793,110 @@ let package = Package( // MARK: Additional Test Dependencies .target( - /** SwiftPM test support library */ - name: "SPMTestSupport", + /** SwiftPM internal build test suite support library */ + name: "_InternalBuildTestSupport", dependencies: [ - "Basics", "Build", + "XCBuildSupport", + "SwiftBuildSupport", + "_InternalTestSupport" + ], + swiftSettings: [ + .unsafeFlags(["-static"]), + ] + ), + + .target( + /** SwiftPM internal test suite support library */ + name: "_InternalTestSupport", + dependencies: [ + "Basics", + "DriverSupport", "PackageFingerprint", "PackageGraph", "PackageLoading", "PackageRegistry", "PackageSigning", "SourceControl", - .product(name: "TSCTestSupport", package: "swift-tools-support-core"), + .product(name: "OrderedCollections", package: "swift-collections"), "Workspace", - "XCBuildSupport", + ] + swiftTSCTestSupportDeps, + swiftSettings: [ + .unsafeFlags(["-static"]), ] ), + .target( + /** SwiftPM internal test suite support library */ + name: "_IntegrationTestSupport", + dependencies: [ + "_InternalTestSupport", + ] + swiftTSCTestSupportDeps, + ), .target( - /** Test for thread-santizer. */ + /** Test for thread-sanitizer. */ name: "tsan_utils", - dependencies: []), + dependencies: [], + swiftSettings: [ + .unsafeFlags(["-static"]), + ] + ), // MARK: SwiftPM tests + .testTarget( + name: "_AsyncFileSystemTests", + dependencies: [ + "_AsyncFileSystem", + "_InternalTestSupport", + ] + ), + .testTarget( name: "SourceKitLSPAPITests", dependencies: [ "SourceKitLSPAPI", - "SPMTestSupport", + "_InternalTestSupport", ] ), .testTarget( name: "BasicsTests", - dependencies: ["Basics", "SPMTestSupport", "tsan_utils"], + dependencies: [ + "Basics", + "_InternalTestSupport", + "tsan_utils", + ], exclude: [ "Archiver/Inputs/archive.tar.gz", "Archiver/Inputs/archive.zip", "Archiver/Inputs/invalid_archive.tar.gz", "Archiver/Inputs/invalid_archive.zip", + "processInputs/long-stdout-stderr", + "processInputs/long-stdout-stderr.bat", + "processInputs/exit4", + "processInputs/exit4.bat", + "processInputs/simple-stdout-stderr", + "processInputs/simple-stdout-stderr.bat", + "processInputs/deadlock-if-blocking-io", + "processInputs/deadlock-if-blocking-io.bat", + "processInputs/echo", + "processInputs/echo.bat", + "processInputs/in-to-out", + "processInputs/in-to-out.bat", ] ), .testTarget( name: "BuildTests", - dependencies: ["Build", "PackageModel", "SPMTestSupport"] + dependencies: ["Build", "PackageModel", "Commands", "_InternalTestSupport", "_InternalBuildTestSupport"] ), .testTarget( name: "LLBuildManifestTests", - dependencies: ["Basics", "LLBuildManifest", "SPMTestSupport"] + dependencies: ["Basics", "LLBuildManifest", "_InternalTestSupport"] ), .testTarget( name: "WorkspaceTests", - dependencies: ["Workspace", "SPMTestSupport"] + dependencies: ["Workspace", "_InternalTestSupport"] ), .testTarget( name: "PackageDescriptionTests", @@ -583,28 +904,25 @@ let package = Package( ), .testTarget( name: "SPMBuildCoreTests", - dependencies: ["SPMBuildCore", "SPMTestSupport"] + dependencies: ["SPMBuildCore", "_InternalTestSupport"] ), .testTarget( name: "PackageLoadingTests", - dependencies: ["PackageLoading", "SPMTestSupport"], + dependencies: ["PackageLoading", "_InternalTestSupport"], exclude: ["Inputs", "pkgconfigInputs"] ), - .testTarget( - name: "PackageLoadingPerformanceTests", - dependencies: ["PackageLoading", "SPMTestSupport"] - ), .testTarget( name: "PackageModelTests", - dependencies: ["PackageModel", "SPMTestSupport"] + dependencies: ["PackageModel", "_InternalTestSupport"] ), .testTarget( name: "PackageGraphTests", - dependencies: ["PackageGraph", "SPMTestSupport"] + dependencies: ["PackageGraph", "_InternalTestSupport"], + swiftSettings: commonExperimentalFeatures ), .testTarget( name: "PackageGraphPerformanceTests", - dependencies: ["PackageGraph", "SPMTestSupport"], + dependencies: ["PackageGraph", "_InternalTestSupport"], exclude: [ "Inputs/PerfectHTTPServer.json", "Inputs/ZewoHTTPServer.json", @@ -614,42 +932,65 @@ let package = Package( ), .testTarget( name: "PackageCollectionsModelTests", - dependencies: ["PackageCollectionsModel", "SPMTestSupport"] + dependencies: ["PackageCollectionsModel", "_InternalTestSupport"] ), .testTarget( name: "PackageCollectionsSigningTests", - dependencies: ["PackageCollectionsSigning", "SPMTestSupport"] + dependencies: ["PackageCollectionsSigning", "_InternalTestSupport"] ), .testTarget( name: "PackageCollectionsTests", - dependencies: ["PackageCollections", "SPMTestSupport", "tsan_utils"] + dependencies: ["PackageCollections", "_InternalTestSupport", "tsan_utils"] ), .testTarget( name: "PackageFingerprintTests", - dependencies: ["PackageFingerprint", "SPMTestSupport"] + dependencies: ["PackageFingerprint", "_InternalTestSupport"] ), .testTarget( name: "PackagePluginAPITests", - dependencies: ["PackagePlugin", "SPMTestSupport"] + dependencies: ["PackagePlugin", "_InternalTestSupport"] ), .testTarget( name: "PackageRegistryTests", - dependencies: ["SPMTestSupport", "PackageRegistry"] + dependencies: ["_InternalTestSupport", "PackageRegistry"] ), .testTarget( name: "PackageSigningTests", - dependencies: ["SPMTestSupport", "PackageSigning"] + dependencies: ["_InternalTestSupport", "PackageSigning"] + ), + .testTarget( + name: "QueryEngineTests", + dependencies: ["QueryEngine", "_InternalTestSupport"] ), .testTarget( name: "SourceControlTests", - dependencies: ["SourceControl", "SPMTestSupport"], + dependencies: ["SourceControl", "_InternalTestSupport"], exclude: ["Inputs/TestRepo.tgz"] ), + .testTarget( + name: "SwiftFixItTests", + dependencies: ["SwiftFixIt", "_InternalTestSupport"] + ), + .testTarget( + name: "BinarySymbolsTests", + dependencies: ["BinarySymbols", "_InternalTestSupport"] + ), .testTarget( name: "XCBuildSupportTests", - dependencies: ["XCBuildSupport", "SPMTestSupport"], + dependencies: ["XCBuildSupport", "_InternalTestSupport", "_InternalBuildTestSupport"], exclude: ["Inputs/Foo.pc"] ), + .testTarget( + name: "FunctionalPerformanceTests", + dependencies: [ + "swift-package-manager", + "_InternalTestSupport", + ] + ), + .testTarget( + name: "SwiftBuildSupportTests", + dependencies: ["SwiftBuildSupport", "_InternalTestSupport", "_InternalBuildTestSupport"] + ), // Examples (These are built to ensure they stay up to date with the API.) .executableTarget( name: "package-info", @@ -657,66 +998,80 @@ let package = Package( path: "Examples/package-info/Sources/package-info" ) ], - swiftLanguageVersions: [.v5] + swiftLanguageModes: [.v5] ) -// Workaround SPM's attempt to link in executables which does not work on all -// platforms. -#if !os(Windows) +#if canImport(Darwin) package.targets.append(contentsOf: [ - .testTarget( - name: "FunctionalPerformanceTests", - dependencies: [ - "swift-build", - "swift-package", - "swift-test", - "SPMTestSupport" - ] - ), + .executableTarget( + name: "swiftpm-testing-helper" + ) ]) +#endif -// rdar://101868275 "error: cannot find 'XCTAssertEqual' in scope" can affect almost any functional test, so we flat out disable them all until we know what is going on +// rdar://101868275 "error: cannot find 'XCTAssertEqual' in scope" can affect almost any functional test, so we flat out +// disable them all until we know what is going on if ProcessInfo.processInfo.environment["SWIFTCI_DISABLE_SDK_DEPENDENT_TESTS"] == nil { package.targets.append(contentsOf: [ .testTarget( name: "FunctionalTests", dependencies: [ - "swift-build", - "swift-package", - "swift-test", + "swift-package-manager", "PackageModel", - "SPMTestSupport" + "_InternalTestSupport", ] ), - .executableTarget( name: "dummy-swiftc", dependencies: [ "Basics", ] ), - + .testTarget( + name: "_InternalTestSupportTests", + dependencies: [ + "_InternalTestSupport" + ] + ), + .testTarget( + name: "IntegrationTests", + dependencies: [ + "_IntegrationTestSupport", + "_InternalTestSupport", + ] + swiftTSCTestSupportDeps + swiftToolsCoreSupportAutoDeps, + ), .testTarget( name: "CommandsTests", dependencies: [ - "swift-build", - "swift-package", - "swift-test", - "swift-run", + "swift-package-manager", "Basics", "Build", "Commands", "PackageModel", - "PackageRegistryTool", + "PackageRegistryCommand", "SourceControl", - "SPMTestSupport", + "_InternalTestSupport", "Workspace", "dummy-swiftc", ] ), ]) } -#endif + + +func swiftSyntaxDependencies(_ names: [String]) -> [Target.Dependency] { + /// Whether swift-syntax is being built as a single dynamic library instead of as a separate library per module. + /// + /// This means that the swift-syntax symbols don't need to be statically linked, which allows us to stay below the + /// maximum number of exported symbols on Windows, in turn allowing us to build sourcekit-lsp using SwiftPM on Windows + /// and run its tests. + let buildDynamicSwiftSyntaxLibrary = ProcessInfo.processInfo.environment["SWIFTSYNTAX_BUILD_DYNAMIC_LIBRARY"] != nil + if buildDynamicSwiftSyntaxLibrary { + return [.product(name: "_SwiftSyntaxDynamic", package: "swift-syntax")] + } else { + return names.map { .product(name: $0, package: "swift-syntax") } + } +} // Add package dependency on llbuild when not bootstrapping. // @@ -733,7 +1088,7 @@ let relatedDependenciesBranch = "main" if ProcessInfo.processInfo.environment["SWIFTPM_LLBUILD_FWK"] == nil { if ProcessInfo.processInfo.environment["SWIFTCI_USE_LOCAL_DEPS"] == nil { package.dependencies += [ - .package(url: "https://github.com/apple/swift-llbuild.git", branch: relatedDependenciesBranch), + .package(url: "https://github.com/swiftlang/swift-llbuild.git", branch: relatedDependenciesBranch), ] } else { // In Swift CI, use a local path to llbuild to interoperate with tools @@ -749,25 +1104,70 @@ if ProcessInfo.processInfo.environment["SWIFTPM_LLBUILD_FWK"] == nil { if ProcessInfo.processInfo.environment["SWIFTCI_USE_LOCAL_DEPS"] == nil { package.dependencies += [ - .package(url: "https://github.com/apple/swift-tools-support-core.git", branch: relatedDependenciesBranch), - // The 'swift-argument-parser' version declared here must match that - // used by 'swift-driver' and 'sourcekit-lsp'. Please coordinate - // dependency version changes here with those projects. - .package(url: "https://github.com/apple/swift-argument-parser.git", .upToNextMinor(from: "1.2.2")), - .package(url: "https://github.com/apple/swift-driver.git", branch: relatedDependenciesBranch), - .package(url: "https://github.com/apple/swift-crypto.git", .upToNextMinor(from: "3.0.0")), - .package(url: "https://github.com/apple/swift-system.git", .upToNextMinor(from: "1.1.1")), - .package(url: "https://github.com/apple/swift-collections.git", .upToNextMinor(from: "1.0.1")), - .package(url: "https://github.com/apple/swift-certificates.git", .upToNextMinor(from: "1.0.1")), + // These need to match the versions in the swiftlang/swift repo, + // utils/update_checkout/update-checkout-config.json + // They are used to build the official swift toolchain. + .package(url: "https://github.com/swiftlang/swift-syntax.git", branch: relatedDependenciesBranch), + .package(url: "https://github.com/apple/swift-argument-parser.git", revision: "1.5.1"), + .package(url: "https://github.com/apple/swift-crypto.git", revision: "3.12.5"), + .package(url: "https://github.com/apple/swift-system.git", revision: "1.5.0"), + .package(url: "https://github.com/apple/swift-collections.git", revision: "1.1.6"), + .package(url: "https://github.com/apple/swift-certificates.git", revision: "1.10.1"), + .package(url: "https://github.com/swiftlang/swift-toolchain-sqlite.git", revision: "1.0.7"), + // Not in toolchain, used for use in previewing documentation + .package(url: "https://github.com/swiftlang/swift-docc-plugin", from: "1.1.0"), ] + if !swiftDriverDeps.isEmpty { + package.dependencies += [ + .package(url: "https://github.com/swiftlang/swift-tools-support-core.git", branch: relatedDependenciesBranch), + .package(url: "https://github.com/swiftlang/swift-driver.git", branch: relatedDependenciesBranch), + ] + } } else { package.dependencies += [ - .package(path: "../swift-tools-support-core"), .package(path: "../swift-argument-parser"), - .package(path: "../swift-driver"), .package(path: "../swift-crypto"), + .package(path: "../swift-syntax"), .package(path: "../swift-system"), .package(path: "../swift-collections"), .package(path: "../swift-certificates"), + .package(path: "../swift-toolchain-sqlite"), ] + if !swiftDriverDeps.isEmpty { + package.dependencies += [ + .package(path: "../swift-tools-support-core"), + .package(path: "../swift-driver"), + ] + } + +} + +/// If ENABLE_APPLE_PRODUCT_TYPES is set in the environment, then also define ENABLE_APPLE_PRODUCT_TYPES in each of the regular targets and test targets. +if ProcessInfo.processInfo.environment["ENABLE_APPLE_PRODUCT_TYPES"] == "1" { + for target in package.targets.filter({ $0.type == .regular || $0.type == .test }) { + target.swiftSettings = (target.swiftSettings ?? []) + [ .define("ENABLE_APPLE_PRODUCT_TYPES") ] + } +} + +if !shouldUseSwiftBuildFramework { + + let swiftbuildsupport: Target = package.targets.first(where: { $0.name == "SwiftBuildSupport" } )! + swiftbuildsupport.dependencies += [ + .product(name: "SwiftBuild", package: "swift-build"), + ] + + swiftbuildsupport.dependencies += [ + // This is here to statically link the build service in the same executable as SwiftPM + .product(name: "SWBBuildService", package: "swift-build"), + ] + + if ProcessInfo.processInfo.environment["SWIFTCI_USE_LOCAL_DEPS"] == nil { + package.dependencies += [ + .package(url: "https://github.com/swiftlang/swift-build.git", branch: relatedDependenciesBranch), + ] + } else { + package.dependencies += [ + .package(path: "../swift-build"), + ] + } } diff --git a/README.md b/README.md index 4525450fea0..eb35c7024a7 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ We’ve designed the system to make it easy to share packages on services like G Swift Package Manager includes a build system that can build for macOS and Linux. Starting with Xcode 11, Xcode integrates with SwiftPM to provide support for including packages in iOS, macOS, watchOS, and tvOS applications. -The [SourceKit-LSP](https://github.com/apple/sourcekit-lsp) project leverages libSwiftPM and provides [Language Server Protocol](https://langserver.org/) implementation for editors that support LSP. +The [SourceKit-LSP](https://github.com/swiftlang/sourcekit-lsp) project leverages libSwiftPM and provides [Language Server Protocol](https://langserver.org/) implementation for editors that support LSP. --- @@ -23,7 +23,7 @@ The [SourceKit-LSP](https://github.com/apple/sourcekit-lsp) project leverages li ## Getting Started -Please use [this guide](https://swift.org/getting-started/#using-the-package-manager) for learning package manager basics. +Please use [this guide](https://www.swift.org/documentation/package-manager/) for learning package manager basics. --- @@ -31,11 +31,11 @@ Please use [this guide](https://swift.org/getting-started/#using-the-package-man For Quick Help use the `swift package --help` command. -For documentation on using Swift Package Manager, creating packages, and more, see the [documentation directory](Documentation/README.md). +For documentation on using Swift Package Manager, creating packages, and more, see the [Package Manager Docs](https://docs.swift.org/swiftpm/documentation/packagemanagerdocs). For documentation on developing the Swift Package Manager itself, see the [contribution guide](CONTRIBUTING.md). -For detailed documentation on the package manifest API, see [PackageDescription API](https://docs.swift.org/package-manager/PackageDescription/index.html). +For detailed documentation on the package manifest API, see [PackageDescription API](https://docs.swift.org/swiftpm/documentation/packagedescription). For release notes with information about changes between versions, see the [release notes](Documentation/ReleaseNotes). @@ -43,7 +43,7 @@ For release notes with information about changes between versions, see the [rele ## System Requirements -The package manager’s system requirements are the same as [those for Swift](https://github.com/apple/swift#system-requirements) with the caveat that the package manager requires Git at runtime as well as build-time. +The package manager’s system requirements are the same as [those for Swift](https://github.com/swiftlang/swift/blob/main/docs/HowToGuides/GettingStarted.md#system-requirements) with the caveat that the package manager requires Git at runtime as well as build-time. --- @@ -73,7 +73,7 @@ The Swift package manager uses [llbuild](https://github.com/apple/swift-llbuild) If you have any trouble with the package manager, help is available. We recommend: * The [Swift Forums](https://forums.swift.org/c/development/swiftpm/), -* SwiftPM's [bug tracker](https://github.com/apple/swift-package-manager/issues) +* SwiftPM's [bug tracker](https://github.com/swiftlang/swift-package-manager/issues) When reporting an issue please follow the bug reporting guidelines, they can be found in [contribution guide](./CONTRIBUTING.md#reporting-issues). @@ -83,7 +83,7 @@ If you’re not comfortable sharing your question with the list, contact details ## License -Copyright 2015 - 2023 Apple Inc. and the Swift project authors. Licensed under Apache License v2.0 with Runtime Library Exception. +Copyright 2015 - 2025 Apple Inc. and the Swift project authors. Licensed under Apache License v2.0 with Runtime Library Exception. See [https://swift.org/LICENSE.txt](https://swift.org/LICENSE.txt) for license information. diff --git a/Sources/AppleProductTypes/Product.swift b/Sources/AppleProductTypes/Product.swift new file mode 100644 index 00000000000..23f52a0a850 --- /dev/null +++ b/Sources/AppleProductTypes/Product.swift @@ -0,0 +1,90 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2021 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 Swift project authors +*/ + +@_spi(PackageProductSettings) import PackageDescription + +#if ENABLE_APPLE_PRODUCT_TYPES +extension Product { + /// Creates an iOS application package product. + /// + /// - Parameters: + /// - name: The name of the application product. + /// - targets: The targets to include in the application product; one and only one of them should be an executable target. + /// - settings: The settings that define the core properties of the application. + public static func iOSApplication( + name: String, + targets: [String], + bundleIdentifier: String? = nil, + teamIdentifier: String? = nil, + displayVersion: String? = nil, + bundleVersion: String? = nil, + iconAssetName: String? = nil, + accentColorAssetName: String? = nil, + supportedDeviceFamilies: [ProductSetting.IOSAppInfo.DeviceFamily], + supportedInterfaceOrientations: [ProductSetting.IOSAppInfo.InterfaceOrientation], + capabilities: [ProductSetting.IOSAppInfo.Capability] = [], + additionalInfoPlistContentFilePath: String? = nil + ) -> Product { + return iOSApplication( + name: name, + targets: targets, + bundleIdentifier: bundleIdentifier, + teamIdentifier: teamIdentifier, + displayVersion: displayVersion, + bundleVersion: bundleVersion, + appIcon: iconAssetName.map({ .asset($0) }), + accentColor: accentColorAssetName.map({ .asset($0) }), + supportedDeviceFamilies: supportedDeviceFamilies, + supportedInterfaceOrientations: supportedInterfaceOrientations, + capabilities: capabilities, + additionalInfoPlistContentFilePath: additionalInfoPlistContentFilePath + ) + } + + /// Creates an iOS application package product. + /// + /// - Parameters: + /// - name: The name of the application product. + /// - targets: The targets to include in the application product; one and only one of them should be an executable target. + /// - settings: The settings that define the core properties of the application. + @available(_PackageDescription, introduced: 5.6) + public static func iOSApplication( + name: String, + targets: [String], + bundleIdentifier: String? = nil, + teamIdentifier: String? = nil, + displayVersion: String? = nil, + bundleVersion: String? = nil, + appIcon: ProductSetting.IOSAppInfo.AppIcon? = nil, + accentColor: ProductSetting.IOSAppInfo.AccentColor? = nil, + supportedDeviceFamilies: [ProductSetting.IOSAppInfo.DeviceFamily], + supportedInterfaceOrientations: [ProductSetting.IOSAppInfo.InterfaceOrientation], + capabilities: [ProductSetting.IOSAppInfo.Capability] = [], + appCategory: ProductSetting.IOSAppInfo.AppCategory? = nil, + additionalInfoPlistContentFilePath: String? = nil + ) -> Product { + return .executable(name: name, targets: targets, settings: [ + bundleIdentifier.map{ .bundleIdentifier($0) }, + teamIdentifier.map{ .teamIdentifier($0) }, + displayVersion.map{ .displayVersion($0) }, + bundleVersion.map{ .bundleVersion($0) }, + .iOSAppInfo(ProductSetting.IOSAppInfo( + appIcon: appIcon, + accentColor: accentColor, + supportedDeviceFamilies: supportedDeviceFamilies, + supportedInterfaceOrientations: supportedInterfaceOrientations, + capabilities: capabilities, + appCategory: appCategory, + additionalInfoPlistContentFilePath: additionalInfoPlistContentFilePath + )) + ].compactMap{ $0 }) + } +} +#endif diff --git a/Sources/Basics/Archiver/Archiver.swift b/Sources/Basics/Archiver/Archiver.swift index b76130b1500..7134860dd20 100644 --- a/Sources/Basics/Archiver/Archiver.swift +++ b/Sources/Basics/Archiver/Archiver.swift @@ -11,9 +11,10 @@ //===----------------------------------------------------------------------===// import _Concurrency +import struct Foundation.URL /// The `Archiver` protocol abstracts away the different operations surrounding archives. -public protocol Archiver { +public protocol Archiver: Sendable { /// A set of extensions the current archiver supports. var supportedExtensions: Set { get } @@ -27,7 +28,7 @@ public protocol Archiver { func extract( from archivePath: AbsolutePath, to destinationPath: AbsolutePath, - completion: @escaping (Result) -> Void + completion: @escaping @Sendable (Result) -> Void ) /// Asynchronously compress the contents of a directory to a destination archive. @@ -35,13 +36,10 @@ public protocol Archiver { /// - Parameters: /// - directory: The `AbsolutePath` to the archive to extract. /// - destinationPath: The `AbsolutePath` to the directory to extract to. - /// - completion: The completion handler that will be called when the operation finishes to notify of its success. - @available(*, noasync, message: "Use the async alternative") func compress( directory: AbsolutePath, - to destinationPath: AbsolutePath, - completion: @escaping (Result) -> Void - ) + to destinationPath: AbsolutePath + ) async throws /// Asynchronously validates if a file is an archive. /// @@ -51,7 +49,7 @@ public protocol Archiver { @available(*, noasync, message: "Use the async alternative") func validate( path: AbsolutePath, - completion: @escaping (Result) -> Void + completion: @escaping @Sendable (Result) -> Void ) } @@ -65,22 +63,8 @@ extension Archiver { from archivePath: AbsolutePath, to destinationPath: AbsolutePath ) async throws { - try await withCheckedThrowingContinuation { - self.extract(from: archivePath, to: destinationPath, completion: $0.resume(with:)) - } - } - - /// Asynchronously compresses the contents of a directory to a destination archive. - /// - /// - Parameters: - /// - directory: The `AbsolutePath` to the archive to extract. - /// - destinationPath: The `AbsolutePath` to the directory to extract to. - public func compress( - directory: AbsolutePath, - to destinationPath: AbsolutePath - ) async throws { - try await withCheckedThrowingContinuation { - self.compress(directory: directory, to: destinationPath, completion: $0.resume(with:)) + try await withCheckedThrowingContinuation { continuation in + self.extract(from: archivePath, to: destinationPath, completion: { continuation.resume(with: $0) }) } } @@ -91,8 +75,12 @@ extension Archiver { public func validate( path: AbsolutePath ) async throws -> Bool { - try await withCheckedThrowingContinuation { - self.validate(path: path, completion: $0.resume(with:)) + try await withCheckedThrowingContinuation { continuation in + self.validate(path: path, completion: { continuation.resume(with: $0) }) } } + + package func isFileSupported(_ lastPathComponent: String) -> Bool { + self.supportedExtensions.contains(where: { lastPathComponent.hasSuffix($0) }) + } } diff --git a/Sources/Basics/Archiver/TarArchiver.swift b/Sources/Basics/Archiver/TarArchiver.swift index d1da7ed0c15..2d1c7fce5b2 100644 --- a/Sources/Basics/Archiver/TarArchiver.swift +++ b/Sources/Basics/Archiver/TarArchiver.swift @@ -13,7 +13,6 @@ import class Dispatch.DispatchQueue import struct Dispatch.DispatchTime import struct TSCBasic.FileSystemError -import class TSCBasic.Process /// An `Archiver` that handles Tar archives using the command-line `tar` tool. public struct TarArchiver: Archiver { @@ -26,7 +25,7 @@ public struct TarArchiver: Archiver { private let cancellator: Cancellator /// The underlying command - private let tarCommand: String + internal let tarCommand: String /// Creates a `TarArchiver`. /// @@ -47,7 +46,7 @@ public struct TarArchiver: Archiver { public func extract( from archivePath: AbsolutePath, to destinationPath: AbsolutePath, - completion: @escaping (Result) -> Void + completion: @escaping @Sendable (Result) -> Void ) { do { guard self.fileSystem.exists(archivePath) else { @@ -58,7 +57,7 @@ public struct TarArchiver: Archiver { throw FileSystemError(.notDirectory, destinationPath.underlying) } - let process = TSCBasic.Process( + let process = AsyncProcess( arguments: [self.tarCommand, "zxf", archivePath.pathString, "-C", destinationPath.pathString] ) @@ -83,45 +82,39 @@ public struct TarArchiver: Archiver { public func compress( directory: AbsolutePath, - to destinationPath: AbsolutePath, - completion: @escaping (Result) -> Void - ) { - do { - guard self.fileSystem.isDirectory(directory) else { - throw FileSystemError(.notDirectory, directory.underlying) - } + to destinationPath: AbsolutePath + ) async throws { - let process = TSCBasic.Process( - arguments: [self.tarCommand, "acf", destinationPath.pathString, directory.basename], - workingDirectory: directory.parentDirectory.underlying - ) + guard self.fileSystem.isDirectory(directory) else { + throw FileSystemError(.notDirectory, directory.underlying) + } - guard let registrationKey = self.cancellator.register(process) else { - throw CancellationError.failedToRegisterProcess(process) - } + let process = AsyncProcess( + arguments: [self.tarCommand, "acf", destinationPath.pathString, directory.basename], + environment: .current, + workingDirectory: directory.parentDirectory + ) - DispatchQueue.sharedConcurrent.async { - defer { self.cancellator.deregister(registrationKey) } - completion(.init(catching: { - try process.launch() - let processResult = try process.waitUntilExit() - guard processResult.exitStatus == .terminated(code: 0) else { - throw try StringError(processResult.utf8stderrOutput()) - } - })) - } - } catch { - return completion(.failure(error)) + guard let registrationKey = self.cancellator.register(process) else { + throw CancellationError.failedToRegisterProcess(process) + } + + defer { self.cancellator.deregister(registrationKey) } + + try process.launch() + let processResult = try await process.waitUntilExit() + guard processResult.exitStatus == .terminated(code: 0) else { + throw try StringError(processResult.utf8stderrOutput()) } } - public func validate(path: AbsolutePath, completion: @escaping (Result) -> Void) { + public func validate(path: AbsolutePath, completion: @escaping @Sendable (Result) -> Void) { do { guard self.fileSystem.exists(path) else { throw FileSystemError(.noEntry, path.underlying) } - let process = TSCBasic.Process(arguments: [self.tarCommand, "tf", path.pathString]) + let process = AsyncProcess(arguments: [self.tarCommand, "tf", path.pathString]) guard let registrationKey = self.cancellator.register(process) else { throw CancellationError.failedToRegisterProcess(process) } diff --git a/Sources/Basics/Archiver/UniversalArchiver.swift b/Sources/Basics/Archiver/UniversalArchiver.swift index cbd5d5d742f..e5736adc4ac 100644 --- a/Sources/Basics/Archiver/UniversalArchiver.swift +++ b/Sources/Basics/Archiver/UniversalArchiver.swift @@ -73,7 +73,7 @@ public struct UniversalArchiver: Archiver { public func extract( from archivePath: AbsolutePath, to destinationPath: AbsolutePath, - completion: @escaping (Result) -> Void + completion: @escaping @Sendable (Result) -> Void ) { do { let archiver = try archiver(for: archivePath) @@ -85,20 +85,15 @@ public struct UniversalArchiver: Archiver { public func compress( directory: AbsolutePath, - to destinationPath: AbsolutePath, - completion: @escaping (Result) -> Void - ) { - do { - let archiver = try archiver(for: destinationPath) - archiver.compress(directory: directory, to: destinationPath, completion: completion) - } catch { - completion(.failure(error)) - } + to destinationPath: AbsolutePath + ) async throws { + let archiver = try archiver(for: destinationPath) + try await archiver.compress(directory: directory, to: destinationPath) } public func validate( path: AbsolutePath, - completion: @escaping (Result) -> Void + completion: @escaping @Sendable (Result) -> Void ) { do { let archiver = try archiver(for: path) diff --git a/Sources/Basics/Archiver/ZipArchiver.swift b/Sources/Basics/Archiver/ZipArchiver.swift index 1bc05ffb8e3..d4f4f1bb7d2 100644 --- a/Sources/Basics/Archiver/ZipArchiver.swift +++ b/Sources/Basics/Archiver/ZipArchiver.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift open source project // -// Copyright (c) 2014-2022 Apple Inc. and the Swift project authors +// Copyright (c) 2014-2023 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 @@ -12,7 +12,10 @@ import Dispatch import struct TSCBasic.FileSystemError -import class TSCBasic.Process + +#if os(Windows) +import WinSDK +#endif /// An `Archiver` that handles ZIP archives using the command-line `zip` and `unzip` tools. public struct ZipArchiver: Archiver, Cancellable { @@ -24,6 +27,18 @@ public struct ZipArchiver: Archiver, Cancellable { /// Helper for cancelling in-flight requests private let cancellator: Cancellator + /// Absolute path to the Windows tar in the system folder + #if os(Windows) + internal let windowsTar: String + #else + internal let unzip = "unzip" + internal let zip = "zip" + #endif + + #if os(FreeBSD) + internal let tar = "tar" + #endif + /// Creates a `ZipArchiver`. /// /// - Parameters: @@ -32,12 +47,25 @@ public struct ZipArchiver: Archiver, Cancellable { public init(fileSystem: FileSystem, cancellator: Cancellator? = .none) { self.fileSystem = fileSystem self.cancellator = cancellator ?? Cancellator(observabilityScope: .none) + + #if os(Windows) + var tarPath: PWSTR? + defer { CoTaskMemFree(tarPath) } + let hr = withUnsafePointer(to: FOLDERID_System) { id in + SHGetKnownFolderPath(id, DWORD(KF_FLAG_DEFAULT.rawValue), nil, &tarPath) + } + if hr == S_OK, let tarPath { + windowsTar = String(decodingCString: tarPath, as: UTF16.self) + "\\tar.exe" + } else { + windowsTar = "tar.exe" + } + #endif } public func extract( from archivePath: AbsolutePath, to destinationPath: AbsolutePath, - completion: @escaping (Result) -> Void + completion: @escaping @Sendable (Result) -> Void ) { do { guard self.fileSystem.exists(archivePath) else { @@ -49,11 +77,13 @@ public struct ZipArchiver: Archiver, Cancellable { } #if os(Windows) - let process = TSCBasic - .Process(arguments: ["tar.exe", "xf", archivePath.pathString, "-C", destinationPath.pathString]) + // FileManager lost the ability to detect tar.exe as executable. + // It's part of system32 anyway so use the absolute path. + let process = AsyncProcess(arguments: [windowsTar, "xf", archivePath.pathString, "-C", destinationPath.pathString]) #else - let process = TSCBasic - .Process(arguments: ["unzip", archivePath.pathString, "-d", destinationPath.pathString]) + let process = AsyncProcess(arguments: [ + self.unzip, archivePath.pathString, "-d", destinationPath.pathString, + ]) #endif guard let registrationKey = self.cancellator.register(process) else { throw CancellationError.failedToRegisterProcess(process) @@ -76,56 +106,67 @@ public struct ZipArchiver: Archiver, Cancellable { public func compress( directory: AbsolutePath, - to destinationPath: AbsolutePath, - completion: @escaping (Result) -> Void - ) { - do { - guard self.fileSystem.isDirectory(directory) else { - throw FileSystemError(.notDirectory, directory.underlying) - } + to destinationPath: AbsolutePath + ) async throws { + guard self.fileSystem.isDirectory(directory) else { + throw FileSystemError(.notDirectory, directory.underlying) + } - #if os(Windows) - let process = TSCBasic.Process( - // FIXME: are these the right arguments? - arguments: ["tar.exe", "-a", "-c", "-f", destinationPath.pathString, directory.basename], - workingDirectory: directory.parentDirectory.underlying - ) - #else - let process = TSCBasic.Process( - arguments: ["zip", "-r", destinationPath.pathString, directory.basename], - workingDirectory: directory.parentDirectory.underlying - ) - #endif + #if os(Windows) + let process = AsyncProcess( + // FIXME: are these the right arguments? + arguments: [windowsTar, "-a", "-c", "-f", destinationPath.pathString, directory.basename], + workingDirectory: directory.parentDirectory + ) + #elseif os(FreeBSD) + // On FreeBSD, the unzip command is available in base but not the zip command. + // Therefore; we use libarchive(bsdtar) to produce the ZIP archive instead. + let process = AsyncProcess( + arguments: [ + self.tar, "-c", "--format", "zip", "-f", destinationPath.pathString, + directory.basename, + ], + workingDirectory: directory.parentDirectory + ) + #else + // This is to work around `swift package-registry publish` tool failing on + // Amazon Linux 2 due to it having an earlier Glibc version (rdar://116370323) + // and therefore posix_spawn_file_actions_addchdir_np is unavailable. + // Instead of passing `workingDirectory` param to TSC.Process, which will trigger + // SPM_posix_spawn_file_actions_addchdir_np_supported check, we shell out and + // do `cd` explicitly before `zip`. + let process = AsyncProcess( + arguments: [ + "/bin/sh", + "-c", + "cd \(directory.parentDirectory.underlying.pathString) && \(self.zip) -ry \(destinationPath.pathString) \(directory.basename)" + ] + ) + #endif + + guard let registrationKey = self.cancellator.register(process) else { + throw CancellationError.failedToRegisterProcess(process) + } - guard let registrationKey = self.cancellator.register(process) else { - throw CancellationError.failedToRegisterProcess(process) - } + defer { self.cancellator.deregister(registrationKey) } - DispatchQueue.sharedConcurrent.async { - defer { self.cancellator.deregister(registrationKey) } - completion(.init(catching: { - try process.launch() - let processResult = try process.waitUntilExit() - guard processResult.exitStatus == .terminated(code: 0) else { - throw try StringError(processResult.utf8stderrOutput()) - } - })) - } - } catch { - return completion(.failure(error)) + try process.launch() + let processResult = try await process.waitUntilExit() + guard processResult.exitStatus == .terminated(code: 0) else { + throw try StringError(processResult.utf8stderrOutput()) } } - public func validate(path: AbsolutePath, completion: @escaping (Result) -> Void) { + public func validate(path: AbsolutePath, completion: @escaping @Sendable (Result) -> Void) { do { guard self.fileSystem.exists(path) else { throw FileSystemError(.noEntry, path.underlying) } #if os(Windows) - let process = TSCBasic.Process(arguments: ["tar.exe", "tf", path.pathString]) + let process = AsyncProcess(arguments: [windowsTar, "tf", path.pathString]) #else - let process = TSCBasic.Process(arguments: ["unzip", "-t", path.pathString]) + let process = AsyncProcess(arguments: [self.unzip, "-t", path.pathString]) #endif guard let registrationKey = self.cancellator.register(process) else { throw CancellationError.failedToRegisterProcess(process) diff --git a/Sources/Basics/ArrayHelpers.swift b/Sources/Basics/ArrayHelpers.swift new file mode 100644 index 00000000000..d6c1dd8e37c --- /dev/null +++ b/Sources/Basics/ArrayHelpers.swift @@ -0,0 +1,73 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +public func nextItem(in array: [T], after item: T) -> T? { + for (index, element) in array.enumerated() { + if element == item { + let nextIndex = index + 1 + return nextIndex < array.count ? array[nextIndex] : nil + } + } + return nil // Item not found or it's the last item +} + +/// Determines if an array contains all elements of a subset array in any order. +/// - Parameters: +/// - array: The array to search within +/// - subset: The subset array to check for +/// - shouldBeContiguous: if `true`, the subset match must be contiguous sequenence +/// - Returns: `true` if all elements in `subset` are present in `array`, `false` otherwise +public func contains(array: [T], subset: [T], shouldBeContiguous: Bool = true) -> Bool { + if shouldBeContiguous { + return containsContiguousSubset(array: array, subset: subset) + } else { + return containsNonContiguousSubset(array: array, subset: subset) + } +} + +/// Determines if an array contains all elements of a subset array in any order. +/// - Parameters: +/// - array: The array to search within +/// - subset: The subset array to check for +/// - Returns: `true` if all elements in `subset` are present in `array`, `false` otherwise +internal func containsNonContiguousSubset(array: [T], subset: [T]) -> Bool { + for element in subset { + if !array.contains(element) { + return false + } + } + return true +} + +/// Determines if an array contains a contiguous subsequence matching the subset array. +/// - Parameters: +/// - array: The array to search within +/// - subset: The contiguous subset array to check for +/// - Returns: `true` if `subset` appears as a contiguous subsequence in `array`, `false` otherwise +internal func containsContiguousSubset(array: [T], subset: [T]) -> Bool { + guard !subset.isEmpty else { return true } + guard subset.count <= array.count else { return false } + + for startIndex in 0...(array.count - subset.count) { + var matches = true + for (offset, element) in subset.enumerated() { + if array[startIndex + offset] != element { + matches = false + break + } + } + if matches { + return true + } + } + return false +} diff --git a/Sources/Basics/AuthorizationProvider.swift b/Sources/Basics/AuthorizationProvider.swift index 11e80cb79ed..85e7b1a7c51 100644 --- a/Sources/Basics/AuthorizationProvider.swift +++ b/Sources/Basics/AuthorizationProvider.swift @@ -10,6 +10,7 @@ // //===----------------------------------------------------------------------===// +import TSCBasic import struct Foundation.Data import struct Foundation.Date import struct Foundation.URL @@ -17,47 +18,19 @@ import struct Foundation.URL import Security #endif -public protocol AuthorizationProvider { - @Sendable +public protocol AuthorizationProvider: Sendable { func authentication(for url: URL) -> (user: String, password: String)? } public protocol AuthorizationWriter { - @available(*, noasync, message: "Use the async alternative") func addOrUpdate( for url: URL, user: String, password: String, - persist: Bool, - callback: @escaping (Result) -> Void - ) - - @available(*, noasync, message: "Use the async alternative") - func remove(for url: URL, callback: @escaping (Result) -> Void) -} + persist: Bool + ) async throws -public extension AuthorizationWriter { - func addOrUpdate( - for url: URL, - user: String, - password: String, - persist: Bool = true - ) async throws { - try await safe_async { - self.addOrUpdate( - for: url, - user: user, - password: password, - persist: persist, - callback: $0) - } - } - - func remove(for url: URL) async throws { - try await safe_async { - self.remove(for: url, callback: $0) - } - } + func remove(for url: URL) async throws } public enum AuthorizationProviderError: Error { @@ -80,7 +53,7 @@ extension AuthorizationProvider { // MARK: - netrc -public class NetrcAuthorizationProvider: AuthorizationProvider, AuthorizationWriter { +public final class NetrcAuthorizationProvider: AuthorizationProvider, AuthorizationWriter { // marked internal for testing internal let path: AbsolutePath private let fileSystem: FileSystem @@ -98,16 +71,15 @@ public class NetrcAuthorizationProvider: AuthorizationProvider, AuthorizationWri for url: URL, user: String, password: String, - persist: Bool = true, - callback: @escaping (Result) -> Void - ) { + persist: Bool = true + ) async throws { guard let machine = Self.machine(for: url) else { - return callback(.failure(AuthorizationProviderError.invalidURLHost)) + throw AuthorizationProviderError.invalidURLHost } if !persist { self.cache[machine] = (user, password) - return callback(.success(())) + return } // Same entry already exists, no need to add or update @@ -115,7 +87,7 @@ public class NetrcAuthorizationProvider: AuthorizationProvider, AuthorizationWri guard netrc?.machines .first(where: { $0.name.lowercased() == machine && $0.login == user && $0.password == password }) == nil else { - return callback(.success(())) + return } do { @@ -132,21 +104,15 @@ public class NetrcAuthorizationProvider: AuthorizationProvider, AuthorizationWri stream.write("\n") } } - - callback(.success(())) } catch { - callback(.failure( - AuthorizationProviderError - .other("Failed to update netrc file at \(self.path): \(error.interpolationDescription)") - )) + throw AuthorizationProviderError + .other("Failed to update netrc file at \(self.path): \(error.interpolationDescription)") } } - public func remove(for url: URL, callback: @escaping (Result) -> Void) { - callback(.failure( - AuthorizationProviderError - .other("User must edit netrc file at \(self.path) manually to remove entries") - )) + public func remove(for url: URL) async throws { + throw AuthorizationProviderError + .other("User must edit netrc file at \(self.path) manually to remove entries") } public func authentication(for url: URL) -> (user: String, password: String)? { @@ -202,7 +168,7 @@ public class NetrcAuthorizationProvider: AuthorizationProvider, AuthorizationWri // MARK: - Keychain #if canImport(Security) -public class KeychainAuthorizationProvider: AuthorizationProvider, AuthorizationWriter { +public final class KeychainAuthorizationProvider: AuthorizationProvider, AuthorizationWriter { private let observabilityScope: ObservabilityScope private let cache = ThreadSafeKeyValueStore() @@ -215,11 +181,10 @@ public class KeychainAuthorizationProvider: AuthorizationProvider, Authorization for url: URL, user: String, password: String, - persist: Bool = true, - callback: @escaping (Result) -> Void - ) { + persist: Bool = true + ) async throws { guard let protocolHostPort = ProtocolHostPort(from: url) else { - return callback(.failure(AuthorizationProviderError.invalidURLHost)) + throw AuthorizationProviderError.invalidURLHost } self.observabilityScope @@ -227,35 +192,25 @@ public class KeychainAuthorizationProvider: AuthorizationProvider, Authorization if !persist { self.cache[protocolHostPort.description] = (user, password) - return callback(.success(())) + return } let passwordData = Data(password.utf8) - do { - if !(try self.update(protocolHostPort: protocolHostPort, account: user, password: passwordData)) { - try self.create(protocolHostPort: protocolHostPort, account: user, password: passwordData) - } - callback(.success(())) - } catch { - callback(.failure(error)) + if !(try self.update(protocolHostPort: protocolHostPort, account: user, password: passwordData)) { + try self.create(protocolHostPort: protocolHostPort, account: user, password: passwordData) } } - public func remove(for url: URL, callback: @escaping (Result) -> Void) { + public func remove(for url: URL) async throws { guard let protocolHostPort = ProtocolHostPort(from: url) else { - return callback(.failure(AuthorizationProviderError.invalidURLHost)) + throw AuthorizationProviderError.invalidURLHost } self.observabilityScope .emit(debug: "remove credentials for '\(protocolHostPort)' [\(url.absoluteString)] from keychain") - do { - try self.delete(protocolHostPort: protocolHostPort) - callback(.success(())) - } catch { - callback(.failure(error)) - } + try self.delete(protocolHostPort: protocolHostPort) } public func authentication(for url: URL) -> (user: String, password: String)? { diff --git a/Sources/Basics/CMakeLists.txt b/Sources/Basics/CMakeLists.txt index dd61eda01eb..e2910d09b81 100644 --- a/Sources/Basics/CMakeLists.txt +++ b/Sources/Basics/CMakeLists.txt @@ -12,26 +12,38 @@ add_library(Basics Archiver/ZipArchiver.swift Archiver/UniversalArchiver.swift AuthorizationProvider.swift - ByteString+Extensions.swift Cancellator.swift + Collections/ByteString+Extensions.swift + Collections/Dictionary+Extensions.swift + Collections/IdentifiableSet.swift + Collections/String+Extensions.swift + Concurrency/AsyncProcess.swift Concurrency/ConcurrencyHelpers.swift Concurrency/NSLock+Extensions.swift Concurrency/SendableBox.swift Concurrency/ThreadSafeArrayStore.swift Concurrency/ThreadSafeBox.swift Concurrency/ThreadSafeKeyValueStore.swift + Concurrency/ThrowingDefer.swift Concurrency/TokenBucket.swift - Dictionary+Extensions.swift DispatchTimeInterval+Extensions.swift - EnvironmentVariables.swift + Environment/Environment.swift + Environment/EnvironmentKey.swift + Environment/EnvironmentShims.swift Errors.swift FileSystem/AbsolutePath.swift FileSystem/FileSystem+Extensions.swift + FileSystem/InMemoryFileSystem.swift FileSystem/NativePathExtensions.swift FileSystem/RelativePath.swift FileSystem/TemporaryFile.swift FileSystem/TSCAdapters.swift + FileSystem/VirtualFileSystem.swift FileSystem/VFSOverlay.swift + Graph/AdjacencyMatrix.swift + Graph/DirectedGraph.swift + Graph/GraphAlgorithms.swift + Graph/UndirectedGraph.swift SourceControlURL.swift HTTPClient/HTTPClient.swift HTTPClient/HTTPClientConfiguration.swift @@ -49,27 +61,35 @@ add_library(Basics Netrc.swift Observability.swift OSSignpost.swift + Process.swift + ProgressAnimation/NinjaProgressAnimation.swift + ProgressAnimation/PercentProgressAnimation.swift + ProgressAnimation/ProgressAnimationProtocol.swift + ProgressAnimation/SingleLinePercentProgressAnimation.swift + ProgressAnimation/ThrottledProgressAnimation.swift SQLite.swift Sandbox.swift SendableTimeInterval.swift Serialization/SerializedJSON.swift - String+Extensions.swift SwiftVersion.swift SQLiteBackedCache.swift + TestingLibrary.swift Triple+Basics.swift + URL.swift Version+Extensions.swift WritableByteStream+Extensions.swift Vendor/Triple.swift Vendor/Triple+Platforms.swift) target_link_libraries(Basics PUBLIC - SwiftCollections::DequeModule + _AsyncFileSystem SwiftCollections::OrderedCollections - SwiftSystem::SystemPackage TSCBasic TSCUtility) target_link_libraries(Basics PRIVATE + SwiftCollections::DequeModule SPMSQLite3 TSCclibc) + # NOTE(compnerd) workaround for CMake not setting up include flags yet set_target_properties(Basics PROPERTIES INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_Swift_MODULE_DIRECTORY}) diff --git a/Sources/Basics/Cancellator.swift b/Sources/Basics/Cancellator.swift index d2c808e88ef..9cb30617d57 100644 --- a/Sources/Basics/Cancellator.swift +++ b/Sources/Basics/Cancellator.swift @@ -12,24 +12,20 @@ import Dispatch import Foundation -import class TSCBasic.Process import class TSCBasic.Thread #if canImport(WinSDK) import WinSDK +#elseif canImport(Android) +import Android #endif -public typealias CancellationHandler = (DispatchTime) throws -> Void +public typealias CancellationHandler = @Sendable (DispatchTime) async throws -> Void -public final class Cancellator: Cancellable { +public final class Cancellator: Cancellable, Sendable { public typealias RegistrationKey = String private let observabilityScope: ObservabilityScope? private let registry = ThreadSafeKeyValueStore() - private let cancelationQueue = DispatchQueue( - label: "org.swift.swiftpm.cancellator", - qos: .userInteractive, - attributes: .concurrent - ) private let cancelling = ThreadSafeBox(false) private static let signalHandlerLock = NSLock() @@ -79,7 +75,7 @@ public final class Cancellator: Cancellable { // Install the default signal handler. var action = sigaction() - #if canImport(Darwin) || os(OpenBSD) + #if canImport(Darwin) || os(OpenBSD) || os(FreeBSD) action.__sigaction_u.__sa_handler = SIG_DFL #elseif canImport(Musl) action.__sa_handler.sa_handler = SIG_DFL @@ -119,15 +115,20 @@ public final class Cancellator: Cancellable { } @discardableResult - public func register(name: String, handler: @escaping () throws -> Void) -> RegistrationKey? { + public func register(name: String, handler: AsyncCancellable) -> RegistrationKey? { + self.register(name: name, handler: handler.cancel(deadline:)) + } + + @discardableResult + public func register(name: String, handler: @escaping @Sendable () throws -> Void) -> RegistrationKey? { self.register(name: name, handler: { _ in try handler() }) } - public func register(_ process: TSCBasic.Process) -> RegistrationKey? { + package func register(_ process: AsyncProcess) -> RegistrationKey? { self.register(name: "\(process.arguments.joined(separator: " "))", handler: process.terminate) } - #if !os(iOS) && !os(watchOS) && !os(tvOS) + #if !canImport(Darwin) || os(macOS) public func register(_ process: Foundation.Process) -> RegistrationKey? { self.register(name: "\(process.description)", handler: process.terminate(timeout:)) } @@ -158,10 +159,12 @@ public final class Cancellator: Cancellable { let cancelled = ThreadSafeArrayStore() let group = DispatchGroup() for (_, (name, handler)) in cancellationHandlers { - self.cancelationQueue.async(group: group) { + group.enter() + Task { + defer { group.leave() } do { self.observabilityScope?.emit(debug: "cancelling '\(name)'") - try handler(handlersDeadline) + try await handler(handlersDeadline) cancelled.append(name) } catch { self.observabilityScope?.emit( @@ -191,6 +194,10 @@ public protocol Cancellable { func cancel(deadline: DispatchTime) throws -> Void } +public protocol AsyncCancellable { + func cancel(deadline: DispatchTime) async throws -> Void +} + public struct CancellationError: Error, CustomStringConvertible { public let description: String @@ -202,7 +209,7 @@ public struct CancellationError: Error, CustomStringConvertible { self.description = description } - static func failedToRegisterProcess(_ process: TSCBasic.Process) -> Self { + static func failedToRegisterProcess(_ process: AsyncProcess) -> Self { Self( description: """ failed to register a cancellation handler for this process invocation `\( @@ -213,7 +220,7 @@ public struct CancellationError: Error, CustomStringConvertible { } } -extension TSCBasic.Process { +extension AsyncProcess { fileprivate func terminate(timeout: DispatchTime) { // send graceful shutdown signal self.signal(SIGINT) @@ -238,7 +245,7 @@ extension TSCBasic.Process { } } -#if !os(iOS) && !os(watchOS) && !os(tvOS) +#if !canImport(Darwin) || os(macOS) extension Foundation.Process { fileprivate func terminate(timeout: DispatchTime) { guard self.isRunning else { diff --git a/Sources/Basics/ByteString+Extensions.swift b/Sources/Basics/Collections/ByteString+Extensions.swift similarity index 100% rename from Sources/Basics/ByteString+Extensions.swift rename to Sources/Basics/Collections/ByteString+Extensions.swift diff --git a/Sources/Basics/Dictionary+Extensions.swift b/Sources/Basics/Collections/Dictionary+Extensions.swift similarity index 98% rename from Sources/Basics/Dictionary+Extensions.swift rename to Sources/Basics/Collections/Dictionary+Extensions.swift index f31e0b57104..f08ff1d15ee 100644 --- a/Sources/Basics/Dictionary+Extensions.swift +++ b/Sources/Basics/Collections/Dictionary+Extensions.swift @@ -10,6 +10,8 @@ // //===----------------------------------------------------------------------===// +import TSCBasic + extension Dictionary { @inlinable @discardableResult diff --git a/Sources/Basics/Collections/IdentifiableSet.swift b/Sources/Basics/Collections/IdentifiableSet.swift new file mode 100644 index 00000000000..70b80355823 --- /dev/null +++ b/Sources/Basics/Collections/IdentifiableSet.swift @@ -0,0 +1,138 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2024 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 struct OrderedCollections.OrderedDictionary + +/// Replacement for `Set` elements that can't be `Hashable`, but can be `Identifiable`. +public struct IdentifiableSet: Collection { + public init() { + self.storage = [:] + } + + public init(_ sequence: some Sequence) { + self.storage = .init(pickLastWhenDuplicateFound: sequence) + } + + fileprivate typealias Storage = OrderedDictionary + + public struct Index: Comparable { + public static func < (lhs: IdentifiableSet.Index, rhs: IdentifiableSet.Index) -> Bool { + lhs.storageIndex < rhs.storageIndex + } + + fileprivate let storageIndex: Storage.Index + } + + private var storage: Storage + + public var startIndex: Index { + Index(storageIndex: self.storage.elements.startIndex) + } + + public var endIndex: Index { + Index(storageIndex: self.storage.elements.endIndex) + } + + public var values: some Sequence { + self.storage.values + } + + public subscript(position: Index) -> Element { + self.storage.elements[position.storageIndex].value + } + + public subscript(id: Element.ID) -> Element? { + get { + self.storage[id] + } + set { + self.storage[id] = newValue + } + } + + public func index(after i: Index) -> Index { + Index(storageIndex: self.storage.elements.index(after: i.storageIndex)) + } + + public mutating func insert(_ element: Element) { + self.storage[element.id] = element + } + + public func union(_ otherSequence: some Sequence) -> Self { + var result = self + for element in otherSequence { + result.storage[element.id] = element + } + return result + } + + public mutating func formUnion(_ otherSequence: some Sequence) { + for element in otherSequence { + self.storage[element.id] = element + } + } + + public func intersection(_ otherSequence: some Sequence) -> Self { + let keysToRemove = Set(self.storage.keys).subtracting(otherSequence.map(\.id)) + var result = self + for key in keysToRemove { + result.storage.removeValue(forKey: key) + } + return result + } + + public func subtracting(_ otherSequence: some Sequence) -> Self { + var result = self + for element in otherSequence { + result.storage.removeValue(forKey: element.id) + } + return result + } + + public func contains(id: Element.ID) -> Bool { + self.storage.keys.contains(id) + } + + public mutating func remove(id: Element.ID) -> Element? { + self.storage.removeValue(forKey: id) + } + + public mutating func remove(_ member: Element) -> Element? { + self.storage.removeValue(forKey: member.id) + } +} + +extension OrderedDictionary where Value: Identifiable, Key == Value.ID { + fileprivate init(pickLastWhenDuplicateFound sequence: some Sequence) { + self.init(sequence.map { ($0.id, $0) }, uniquingKeysWith: { $1 }) + } +} + +extension IdentifiableSet: Equatable { + public static func == (_ lhs: Self, _ rhs: Self) -> Bool { + lhs.storage.keys == rhs.storage.keys + } +} + +extension IdentifiableSet: Hashable { + public func hash(into hasher: inout Hasher) { + for key in self.storage.keys { + hasher.combine(key) + } + } +} + +extension IdentifiableSet: ExpressibleByArrayLiteral { + public init(arrayLiteral elements: Element...) { + self.init(elements) + } +} diff --git a/Sources/Basics/String+Extensions.swift b/Sources/Basics/Collections/String+Extensions.swift similarity index 100% rename from Sources/Basics/String+Extensions.swift rename to Sources/Basics/Collections/String+Extensions.swift diff --git a/Sources/Basics/Concurrency/AsyncProcess.swift b/Sources/Basics/Concurrency/AsyncProcess.swift new file mode 100644 index 00000000000..86222f8fa67 --- /dev/null +++ b/Sources/Basics/Concurrency/AsyncProcess.swift @@ -0,0 +1,1356 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2014 - 2020 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 Swift project authors + */ + +import _Concurrency +import Dispatch +import Foundation + +#if os(Windows) +import TSCLibc +#elseif canImport(Android) +import Android +#endif + +#if !os(Windows) && !canImport(Darwin) +#if USE_IMPL_ONLY_IMPORTS +@_implementationOnly +import func TSCclibc.SPM_posix_spawn_file_actions_addchdir_np_supported + +@_implementationOnly +import func TSCclibc.SPM_posix_spawn_file_actions_addchdir_np +#else +private import func TSCclibc.SPM_posix_spawn_file_actions_addchdir_np_supported +private import func TSCclibc.SPM_posix_spawn_file_actions_addchdir_np +#endif // #if USE_IMPL_ONLY_IMPORTS +#endif + +import class TSCBasic.CStringArray +import class TSCBasic.LocalFileOutputByteStream +import enum TSCBasic.SystemError +import class TSCBasic.Thread +import protocol TSCBasic.WritableByteStream + +/// Process result data which is available after process termination. +package struct AsyncProcessResult: CustomStringConvertible, Sendable { + package enum Error: Swift.Error, Sendable { + /// The output is not a valid UTF8 sequence. + case illegalUTF8Sequence + + /// The process had a non zero exit. + case nonZeroExit(AsyncProcessResult) + + /// The process failed with a `SystemError` (this is used to still provide context on the process that was + /// launched). + case systemError(arguments: [String], underlyingError: Swift.Error) + } + + package enum ExitStatus: Equatable, Sendable { + /// The process was terminated normally with a exit code. + case terminated(code: Int32) + #if os(Windows) + /// The process was terminated abnormally. + case abnormal(exception: UInt32) + #else + /// The process was terminated due to a signal. + case signalled(signal: Int32) + #endif + } + + /// The arguments with which the process was launched. + package let arguments: [String] + + /// The environment with which the process was launched. + package let environment: Environment + + /// The exit status of the process. + package let exitStatus: ExitStatus + + /// The output bytes of the process. Available only if the process was + /// asked to redirect its output and no stdout output closure was set. + package let output: Result<[UInt8], Swift.Error> + + /// The output bytes of the process. Available only if the process was + /// asked to redirect its output and no stderr output closure was set. + package let stderrOutput: Result<[UInt8], Swift.Error> + + /// Create an instance using a POSIX process exit status code and output result. + /// + /// See `waitpid(2)` for information on the exit status code. + package init( + arguments: [String], + environment: Environment, + exitStatusCode: Int32, + normal: Bool, + output: Result<[UInt8], Swift.Error>, + stderrOutput: Result<[UInt8], Swift.Error> + ) { + let exitStatus: ExitStatus + #if os(Windows) + if normal { + exitStatus = .terminated(code: exitStatusCode) + } else { + exitStatus = .abnormal(exception: UInt32(exitStatusCode)) + } + #else + if WIFSIGNALED(exitStatusCode) { + exitStatus = .signalled(signal: WTERMSIG(exitStatusCode)) + } else { + precondition(WIFEXITED(exitStatusCode), "unexpected exit status \(exitStatusCode)") + exitStatus = .terminated(code: WEXITSTATUS(exitStatusCode)) + } + #endif + self.init( + arguments: arguments, + environment: environment, + exitStatus: exitStatus, + output: output, + stderrOutput: stderrOutput + ) + } + + /// Create an instance using an exit status and output result. + package init( + arguments: [String], + environment: Environment, + exitStatus: ExitStatus, + output: Result<[UInt8], Swift.Error>, + stderrOutput: Result<[UInt8], Swift.Error> + ) { + self.arguments = arguments + self.environment = environment + self.output = output + self.stderrOutput = stderrOutput + self.exitStatus = exitStatus + } + + /// Converts stdout output bytes to string, assuming they're UTF8. + package func utf8Output() throws -> String { + try String(decoding: output.get(), as: Unicode.UTF8.self) + } + + /// Converts stderr output bytes to string, assuming they're UTF8. + package func utf8stderrOutput() throws -> String { + try String(decoding: stderrOutput.get(), as: Unicode.UTF8.self) + } + + package var description: String { + """ + + """ + } +} + +extension AsyncProcess: @unchecked Sendable {} + +extension DispatchQueue { + // a shared concurrent queue for running concurrent asynchronous operations + static let processConcurrent = DispatchQueue( + label: "swift.org.swift-tsc.process.concurrent", + attributes: .concurrent + ) +} + +/// Process allows spawning new subprocesses and working with them. +/// +/// Note: This class is thread safe. +package final class AsyncProcess { + /// Errors when attempting to invoke a process + package enum Error: Swift.Error, Sendable { + /// The program requested to be executed cannot be found on the existing search paths, or is not executable. + case missingExecutableProgram(program: String) + + /// The current OS does not support the workingDirectory API. + case workingDirectoryNotSupported + + /// The stdin could not be opened. + case stdinUnavailable + } + + package typealias ReadableStream = AsyncStream<[UInt8]> + + package enum OutputRedirection: Sendable { + /// Do not redirect the output + case none + + /// Collect stdout and stderr output and provide it back via ``AsyncProcessResult`` object. If + /// `redirectStderr` is `true`, `stderr` be redirected to `stdout`. + case collect(redirectStderr: Bool) + + /// Stream `stdout` and `stderr` via the corresponding closures. If `redirectStderr` is `true`, `stderr` will + /// be redirected to `stdout`. + case stream(stdout: OutputClosure, stderr: OutputClosure, redirectStderr: Bool) + + /// Stream stdout and stderr as `AsyncSequence` provided as an argument to closures passed to + /// ``AsyncProcess/launch(stdoutStream:stderrStream:)``. + case asyncStream( + stdoutStream: ReadableStream, + stdoutContinuation: ReadableStream.Continuation, + stderrStream: ReadableStream, + stderrContinuation: ReadableStream.Continuation + ) + + /// Default collect OutputRedirection that defaults to not redirect stderr. Provided for API compatibility. + package static let collect: Self = .collect(redirectStderr: false) + + /// Default stream OutputRedirection that defaults to not redirect stderr. Provided for API compatibility. + package static func stream(stdout: @escaping OutputClosure, stderr: @escaping OutputClosure) -> Self { + .stream(stdout: stdout, stderr: stderr, redirectStderr: false) + } + + package var redirectsOutput: Bool { + switch self { + case .none: + false + case .collect, .stream, .asyncStream: + true + } + } + + package var outputClosures: (stdoutClosure: OutputClosure, stderrClosure: OutputClosure)? { + switch self { + case let .stream(stdoutClosure, stderrClosure, _): + (stdoutClosure: stdoutClosure, stderrClosure: stderrClosure) + + case let .asyncStream(_, stdoutContinuation, _, stderrContinuation): + (stdoutClosure: { stdoutContinuation.yield($0) }, stderrClosure: { stderrContinuation.yield($0) }) + + case .collect, .none: + nil + } + } + + package var redirectStderr: Bool { + switch self { + case .collect(let redirectStderr): + redirectStderr + case .stream(_, _, let redirectStderr): + redirectStderr + default: + false + } + } + } + + // process execution mutable state + private enum State { + case idle + case readingOutput(sync: DispatchGroup) + case outputReady(stdout: Result<[UInt8], Swift.Error>, stderr: Result<[UInt8], Swift.Error>) + case complete(AsyncProcessResult) + case failed(Swift.Error) + } + + /// Typealias for process id type. + #if !os(Windows) + package typealias ProcessID = pid_t + #else + package typealias ProcessID = DWORD + #endif + + /// Typealias for stdout/stderr output closure. + package typealias OutputClosure = ([UInt8]) -> Void + + /// Typealias for logging handling closure + package typealias LoggingHandler = (String) -> Void + + private static var _loggingHandler: LoggingHandler? + private static let loggingHandlerLock = NSLock() + + /// Global logging handler. Use with care! preferably use instance level instead of setting one globally. + @available( + *, + deprecated, + message: "use instance level `loggingHandler` passed via `init` instead of setting one globally." + ) + package static var loggingHandler: LoggingHandler? { + get { + Self.loggingHandlerLock.withLock { + self._loggingHandler + } + } set { + Self.loggingHandlerLock.withLock { + self._loggingHandler = newValue + } + } + } + + package let loggingHandler: LoggingHandler? + + /// The arguments to execute. + package let arguments: [String] + + package let environment: Environment + + /// The path to the directory under which to run the process. + package let workingDirectory: AbsolutePath? + + /// The process id of the spawned process, available after the process is launched. + #if os(Windows) + private var _process: Foundation.Process? + package var processID: ProcessID { + DWORD(self._process?.processIdentifier ?? 0) + } + #else + package private(set) var processID = ProcessID() + #endif + + // process execution mutable state + private var state: State = .idle + private let stateLock = NSLock() + + private static let sharedCompletionQueue = DispatchQueue(label: "org.swift.tools-support-core.process-completion") + private var completionQueue = AsyncProcess.sharedCompletionQueue + + // ideally we would use the state for this, but we need to access it while the waitForExit is locking state + private var _launched = false + private let launchedLock = NSLock() + + package var launched: Bool { + self.launchedLock.withLock { + self._launched + } + } + + /// How process redirects its output. + package let outputRedirection: OutputRedirection + + /// Indicates if a new progress group is created for the child process. + private let startNewProcessGroup: Bool + + /// Cache of validated executables. + /// + /// Key: Executable name or path. + /// Value: Path to the executable, if found. + private static var validatedExecutablesMap = [String: AbsolutePath?]() + private static let validatedExecutablesMapLock = NSLock() + + /// Create a new process instance. + /// + /// - Parameters: + /// - arguments: The arguments for the subprocess. + /// - environment: The environment to pass to subprocess. By default the current process environment + /// will be inherited. + /// - workingDirectory: The path to the directory under which to run the process. + /// - outputRedirection: How process redirects its output. Default value is .collect. + /// - startNewProcessGroup: If true, a new progress group is created for the child making it + /// continue running even if the parent is killed or interrupted. Default value is true. + /// - loggingHandler: Handler for logging messages + /// + package init( + arguments: [String], + environment: Environment = .current, + workingDirectory: AbsolutePath, + outputRedirection: OutputRedirection = .collect, + startNewProcessGroup: Bool = true, + loggingHandler: LoggingHandler? = .none + ) { + self.arguments = arguments + self.environment = environment + self.workingDirectory = workingDirectory + self.outputRedirection = outputRedirection + self.startNewProcessGroup = startNewProcessGroup + self.loggingHandler = loggingHandler ?? AsyncProcess.loggingHandler + } + + /// Create a new process instance. + /// + /// - Parameters: + /// - arguments: The arguments for the subprocess. + /// - environment: The environment to pass to subprocess. By default the current process environment + /// will be inherited. + /// - outputRedirection: How process redirects its output. Default value is .collect. + /// - verbose: If true, launch() will print the arguments of the subprocess before launching it. + /// - startNewProcessGroup: If true, a new progress group is created for the child making it + /// continue running even if the parent is killed or interrupted. Default value is true. + /// - loggingHandler: Handler for logging messages + package init( + arguments: [String], + environment: Environment = .current, + outputRedirection: OutputRedirection = .collect, + startNewProcessGroup: Bool = true, + loggingHandler: LoggingHandler? = .none + ) { + self.arguments = arguments + self.environment = environment + self.workingDirectory = nil + self.outputRedirection = outputRedirection + self.startNewProcessGroup = startNewProcessGroup + self.loggingHandler = loggingHandler ?? AsyncProcess.loggingHandler + } + + package convenience init( + args: [String], + environment: Environment = .current, + outputRedirection: OutputRedirection = .collect, + loggingHandler: LoggingHandler? = .none + ) { + self.init( + arguments: args, + environment: environment, + outputRedirection: outputRedirection, + loggingHandler: loggingHandler + ) + } + + package convenience init( + args: String..., + environment: Environment = .current, + outputRedirection: OutputRedirection = .collect, + loggingHandler: LoggingHandler? = .none + ) { + self.init( + arguments: args, + environment: environment, + outputRedirection: outputRedirection, + loggingHandler: loggingHandler + ) + } + + /// Returns the path of the the given program if found in the search paths. + /// + /// The program can be executable name, relative path or absolute path. + package static func findExecutable( + _ program: String, + workingDirectory: AbsolutePath? = nil + ) -> AbsolutePath? { + if let abs = try? AbsolutePath(validating: program) { + return abs + } + let cwdOpt = workingDirectory ?? localFileSystem.currentWorkingDirectory + // The program might be a multi-component relative path. + if let rel = try? RelativePath(validating: program), rel.components.count > 1 { + if let cwd = cwdOpt { + let abs = AbsolutePath(cwd, rel) + if localFileSystem.isExecutableFile(abs) { + return abs + } + } + return nil + } + // From here on out, the program is an executable name, i.e. it doesn't contain a "/" + let lookup: () -> AbsolutePath? = { + let envSearchPaths = getEnvSearchPaths( + pathString: Environment.current[.path], + currentWorkingDirectory: cwdOpt + ) + let value = lookupExecutablePath( + filename: program, + currentWorkingDirectory: cwdOpt, + searchPaths: envSearchPaths + ) + return value + } + // This should cover the most common cases, i.e. when the cache is most helpful. + if workingDirectory == localFileSystem.currentWorkingDirectory { + return AsyncProcess.validatedExecutablesMapLock.withLock { + if let value = AsyncProcess.validatedExecutablesMap[program] { + return value + } + let value = lookup() + AsyncProcess.validatedExecutablesMap[program] = value + return value + } + } else { + return lookup() + } + } + + /// Launch the subprocess. Returns a WritableByteStream object that can be used to communicate to the process's + /// stdin. If needed, the stream can be closed using the close() API. Otherwise, the stream will be closed + /// automatically. + @discardableResult + package func launch() throws -> any WritableByteStream { + precondition( + self.arguments.count > 0 && !self.arguments[0].isEmpty, + "Need at least one argument to launch the process." + ) + + self.launchedLock.withLock { + precondition(!self._launched, "It is not allowed to launch the same process object again.") + self._launched = true + } + + // Print the arguments if we are verbose. + if let loggingHandler = self.loggingHandler { + loggingHandler(self.arguments.map { $0.spm_shellEscaped() }.joined(separator: " ")) + } + + // Look for executable. + let executable = self.arguments[0] + guard let executablePath = AsyncProcess.findExecutable(executable, workingDirectory: workingDirectory) else { + throw AsyncProcess.Error.missingExecutableProgram(program: executable) + } + + #if os(Windows) + let process = Foundation.Process() + self._process = process + process.arguments = Array(self.arguments.dropFirst()) // Avoid including the executable URL twice. + if let workingDirectory { + process.currentDirectoryURL = workingDirectory.asURL + } + process.executableURL = executablePath.asURL + process.environment = .init(self.environment) + + let stdinPipe = Pipe() + process.standardInput = stdinPipe + + let group = DispatchGroup() + + var stdout: [UInt8] = [] + let stdoutLock = NSLock() + + var stderr: [UInt8] = [] + let stderrLock = NSLock() + + if self.outputRedirection.redirectsOutput { + let stdoutPipe = Pipe() + let stderrPipe = Pipe() + + group.enter() + stdoutPipe.fileHandleForReading.readabilityHandler = { (fh: FileHandle) in + let data = (try? fh.read(upToCount: Int.max)) ?? Data() + if data.count == 0 { + stdoutPipe.fileHandleForReading.readabilityHandler = nil + group.leave() + } else { + let contents = data.withUnsafeBytes { [UInt8]($0) } + self.outputRedirection.outputClosures?.stdoutClosure(contents) + stdoutLock.withLock { + stdout += contents + } + } + } + + group.enter() + stderrPipe.fileHandleForReading.readabilityHandler = { (fh: FileHandle) in + let data = (try? fh.read(upToCount: Int.max)) ?? Data() + if data.count == 0 { + stderrPipe.fileHandleForReading.readabilityHandler = nil + group.leave() + } else { + let contents = data.withUnsafeBytes { [UInt8]($0) } + self.outputRedirection.outputClosures?.stderrClosure(contents) + stderrLock.withLock { + stderr += contents + } + } + } + + process.standardOutput = stdoutPipe + process.standardError = stderrPipe + } + + // first set state then start reading threads + let sync = DispatchGroup() + sync.enter() + self.stateLock.withLock { + self.state = .readingOutput(sync: sync) + } + + group.notify(queue: self.completionQueue) { + self.stateLock.withLock { + self.state = .outputReady(stdout: .success(stdout), stderr: .success(stderr)) + } + sync.leave() + } + + try process.run() + return stdinPipe.fileHandleForWriting + #elseif(!canImport(Darwin) || os(macOS)) + // Initialize the spawn attributes. + #if canImport(Darwin) || os(Android) || os(OpenBSD) || os(FreeBSD) + var attributes: posix_spawnattr_t? = nil + #else + var attributes = posix_spawnattr_t() + #endif + posix_spawnattr_init(&attributes) + defer { posix_spawnattr_destroy(&attributes) } + + // Unmask all signals. + var noSignals = sigset_t() + sigemptyset(&noSignals) + posix_spawnattr_setsigmask(&attributes, &noSignals) + + // Reset all signals to default behavior. + #if canImport(Darwin) + var mostSignals = sigset_t() + sigfillset(&mostSignals) + sigdelset(&mostSignals, SIGKILL) + sigdelset(&mostSignals, SIGSTOP) + posix_spawnattr_setsigdefault(&attributes, &mostSignals) + #else + // On Linux, this can only be used to reset signals that are legal to + // modify, so we have to take care about the set we use. + var mostSignals = sigset_t() + sigemptyset(&mostSignals) + for i in 1 ..< SIGSYS { + if i == SIGKILL || i == SIGSTOP { + continue + } + sigaddset(&mostSignals, i) + } + posix_spawnattr_setsigdefault(&attributes, &mostSignals) + #endif + + // Set the attribute flags. + var flags = POSIX_SPAWN_SETSIGMASK | POSIX_SPAWN_SETSIGDEF + if self.startNewProcessGroup { + // Establish a separate process group. + flags |= POSIX_SPAWN_SETPGROUP + posix_spawnattr_setpgroup(&attributes, 0) + } + + posix_spawnattr_setflags(&attributes, Int16(flags)) + + // Setup the file actions. + #if canImport(Darwin) || os(Android) || os(OpenBSD) || os(FreeBSD) + var fileActions: posix_spawn_file_actions_t? = nil + #else + var fileActions = posix_spawn_file_actions_t() + #endif + posix_spawn_file_actions_init(&fileActions) + defer { posix_spawn_file_actions_destroy(&fileActions) } + + if let workingDirectory = workingDirectory?.pathString { + #if canImport(Darwin) + posix_spawn_file_actions_addchdir_np(&fileActions, workingDirectory) + #else + guard SPM_posix_spawn_file_actions_addchdir_np_supported() else { + throw AsyncProcess.Error.workingDirectoryNotSupported + } + + SPM_posix_spawn_file_actions_addchdir_np(&fileActions, workingDirectory) + #endif + } + + var stdinPipe: [Int32] = [-1, -1] + try open(pipe: &stdinPipe) + + guard let fp = fdopen(stdinPipe[1], "wb") else { + throw AsyncProcess.Error.stdinUnavailable + } + let stdinStream = try LocalFileOutputByteStream(filePointer: fp, closeOnDeinit: true) + + // Dupe the read portion of the remote to 0. + posix_spawn_file_actions_adddup2(&fileActions, stdinPipe[0], 0) + + // Close the other side's pipe since it was dupped to 0. + posix_spawn_file_actions_addclose(&fileActions, stdinPipe[0]) + posix_spawn_file_actions_addclose(&fileActions, stdinPipe[1]) + + var outputPipe: [Int32] = [-1, -1] + var stderrPipe: [Int32] = [-1, -1] + if self.outputRedirection.redirectsOutput { + // Open the pipe. + try open(pipe: &outputPipe) + + // Open the write end of the pipe. + posix_spawn_file_actions_adddup2(&fileActions, outputPipe[1], 1) + + // Close the other ends of the pipe since they were dupped to 1. + posix_spawn_file_actions_addclose(&fileActions, outputPipe[0]) + posix_spawn_file_actions_addclose(&fileActions, outputPipe[1]) + + if self.outputRedirection.redirectStderr { + // If merged was requested, send stderr to stdout. + posix_spawn_file_actions_adddup2(&fileActions, 1, 2) + } else { + // If no redirect was requested, open the pipe for stderr. + try open(pipe: &stderrPipe) + posix_spawn_file_actions_adddup2(&fileActions, stderrPipe[1], 2) + + // Close the other ends of the pipe since they were dupped to 2. + posix_spawn_file_actions_addclose(&fileActions, stderrPipe[0]) + posix_spawn_file_actions_addclose(&fileActions, stderrPipe[1]) + } + } else { + posix_spawn_file_actions_adddup2(&fileActions, 1, 1) + posix_spawn_file_actions_adddup2(&fileActions, 2, 2) + } + + var resolvedArgs = self.arguments + if workingDirectory != nil { + resolvedArgs[0] = executablePath.pathString + } + let argv = CStringArray(resolvedArgs) + let env = CStringArray(environment.map { "\($0.0)=\($0.1)" }) + let rv = posix_spawnp(&self.processID, argv.cArray[0]!, &fileActions, &attributes, argv.cArray, env.cArray) + + guard rv == 0 else { + throw SystemError.posix_spawn(rv, self.arguments) + } + + do { + // Close the local read end of the input pipe. + try close(fd: stdinPipe[0]) + + let group = DispatchGroup() + if !self.outputRedirection.redirectsOutput { + // no stdout or stderr in this case + self.stateLock.withLock { + self.state = .outputReady(stdout: .success([]), stderr: .success([])) + } + } else { + var pending: Result<[UInt8], Swift.Error>? + let pendingLock = NSLock() + + let outputClosures = self.outputRedirection.outputClosures + + // Close the local write end of the output pipe. + try close(fd: outputPipe[1]) + + // Create a thread and start reading the output on it. + group.enter() + let stdoutThread = Thread { [weak self] in + if let readResult = self?.readOutput( + onFD: outputPipe[0], + outputClosure: outputClosures?.stdoutClosure + ) { + pendingLock.withLock { + if let stderrResult = pending { + self?.stateLock.withLock { + self?.state = .outputReady(stdout: readResult, stderr: stderrResult) + } + } else { + pending = readResult + } + } + group.leave() + } else if let stderrResult = (pendingLock.withLock { pending }) { + // TODO: this is more of an error + self?.stateLock.withLock { + self?.state = .outputReady(stdout: .success([]), stderr: stderrResult) + } + group.leave() + } + } + + // Only schedule a thread for stderr if no redirect was requested. + var stderrThread: Thread? = nil + if !self.outputRedirection.redirectStderr { + // Close the local write end of the stderr pipe. + try close(fd: stderrPipe[1]) + + // Create a thread and start reading the stderr output on it. + group.enter() + stderrThread = Thread { [weak self] in + if let readResult = self?.readOutput( + onFD: stderrPipe[0], + outputClosure: outputClosures?.stderrClosure + ) { + pendingLock.withLock { + if let stdoutResult = pending { + self?.stateLock.withLock { + self?.state = .outputReady(stdout: stdoutResult, stderr: readResult) + } + } else { + pending = readResult + } + } + group.leave() + } else if let stdoutResult = (pendingLock.withLock { pending }) { + // TODO: this is more of an error + self?.stateLock.withLock { + self?.state = .outputReady(stdout: stdoutResult, stderr: .success([])) + } + group.leave() + } + } + } else { + pendingLock.withLock { + pending = .success([]) // no stderr in this case + } + } + + // first set state then start reading threads + self.stateLock.withLock { + self.state = .readingOutput(sync: group) + } + + stdoutThread.start() + stderrThread?.start() + } + + return stdinStream + } catch { + throw AsyncProcessResult.Error.systemError(arguments: self.arguments, underlyingError: error) + } + #else + preconditionFailure("Process spawning is not available") + #endif // POSIX implementation + } + + /// Executes the process I/O state machine, returning the result when finished. + @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) + @discardableResult + package func waitUntilExit() async throws -> AsyncProcessResult { + try await withCheckedThrowingContinuation { continuation in + DispatchQueue.processConcurrent.async { + self.waitUntilExit({ + continuation.resume(with: $0) + }) + } + } + } + + /// Blocks the calling process until the subprocess finishes execution. + @available(*, noasync) + @discardableResult + package func waitUntilExit() throws -> AsyncProcessResult { + let group = DispatchGroup() + group.enter() + var processResult: Result? + self.waitUntilExit { result in + processResult = result + group.leave() + } + group.wait() + return try processResult.unsafelyUnwrapped.get() + } + + /// Executes the process I/O state machine, calling completion block when finished. + private func waitUntilExit(_ completion: @escaping (Result) -> Void) { + self.stateLock.lock() + switch self.state { + case .idle: + defer { self.stateLock.unlock() } + preconditionFailure("The process is not yet launched.") + case .complete(let result): + self.stateLock.unlock() + completion(.success(result)) + case .failed(let error): + self.stateLock.unlock() + completion(.failure(error)) + case .readingOutput(let sync): + self.stateLock.unlock() + sync.notify(queue: self.completionQueue) { + self.waitUntilExit(completion) + } + case .outputReady(let stdoutResult, let stderrResult): + defer { self.stateLock.unlock() } + // Wait until process finishes execution. + #if os(Windows) + precondition(self._process != nil, "The process is not yet launched.") + let p = self._process! + p.waitUntilExit() + let exitStatusCode = p.terminationStatus + let normalExit = p.terminationReason == .exit + #else + var exitStatusCode: Int32 = 0 + var result = waitpid(processID, &exitStatusCode, 0) + while result == -1 && errno == EINTR { + result = waitpid(self.processID, &exitStatusCode, 0) + } + if result == -1 { + self.state = .failed(SystemError.waitpid(errno)) + } + let normalExit = !WIFSIGNALED(result) + #endif + + // Construct the result. + let executionResult = AsyncProcessResult( + arguments: arguments, + environment: environment, + exitStatusCode: exitStatusCode, + normal: normalExit, + output: stdoutResult, + stderrOutput: stderrResult + ) + self.state = .complete(executionResult) + self.completionQueue.async { + self.waitUntilExit(completion) + } + } + } + + #if !os(Windows) + /// Reads the given fd and returns its result. + /// + /// Closes the fd before returning. + private func readOutput(onFD fd: Int32, outputClosure: OutputClosure?) -> Result<[UInt8], Swift.Error> { + // Read all of the data from the output pipe. + let N = 4096 + var buf = [UInt8](repeating: 0, count: N + 1) + + var out = [UInt8]() + var error: Swift.Error? = nil + loop: while true { + let n = read(fd, &buf, N) + switch n { + case -1: + if errno == EINTR { + continue + } else { + error = SystemError.read(errno) + break loop + } + case 0: + // Close the read end of the output pipe. + // We should avoid closing the read end of the pipe in case + // -1 because the child process may still have content to be + // flushed into the write end of the pipe. If the read end of the + // pipe is closed, then a write will cause a SIGPIPE signal to + // be generated for the calling process. If the calling process is + // ignoring this signal, then write fails with the error EPIPE. + close(fd) + break loop + default: + let data = buf[0 ..< n] + if let outputClosure { + outputClosure(Array(data)) + } else { + out += data + } + } + } + // Construct the output result. + return error.map(Result.failure) ?? .success(out) + } + #endif + + /// Send a signal to the process. + /// + /// Note: This will signal all processes in the process group. + package func signal(_ signal: Int32) { + #if os(Windows) + if signal == SIGINT { + self._process?.interrupt() + } else { + self._process?.terminate() + } + #else + assert(self.launched, "The process is not yet launched.") + kill(self.startNewProcessGroup ? -self.processID : self.processID, signal) + #endif + } +} + +extension AsyncProcess { + /// Execute a subprocess and returns the result when it finishes execution + /// + /// - Parameters: + /// - arguments: The arguments for the subprocess. + /// - environment: The environment to pass to subprocess. By default the current process environment + /// will be inherited. + /// - loggingHandler: Handler for logging messages + package static func popen( + arguments: [String], + environment: Environment = .current, + loggingHandler: LoggingHandler? = .none + ) async throws -> AsyncProcessResult { + let process = AsyncProcess( + arguments: arguments, + environment: environment, + outputRedirection: .collect, + loggingHandler: loggingHandler + ) + try process.launch() + return try await process.waitUntilExit() + } + + /// Execute a subprocess and returns the result when it finishes execution + /// + /// - Parameters: + /// - args: The arguments for the subprocess. + /// - environment: The environment to pass to subprocess. By default the current process environment + /// will be inherited. + /// - loggingHandler: Handler for logging messages + package static func popen( + args: String..., + environment: Environment = .current, + loggingHandler: LoggingHandler? = .none + ) async throws -> AsyncProcessResult { + try await self.popen(arguments: args, environment: environment, loggingHandler: loggingHandler) + } + + package typealias DuplexStreamHandler = + @Sendable (_ stdinStream: WritableByteStream, _ stdoutStream: ReadableStream) async throws -> () + package typealias ReadableStreamHandler = + @Sendable (_ stderrStream: ReadableStream) async throws -> () + + /// Launches a new `AsyncProcess` instances, allowing the caller to consume `stdout` and `stderr` output + /// with handlers that support structured concurrency. + /// - Parameters: + /// - arguments: CLI command used to launch the process. + /// - environment: environment variables passed to the launched process. + /// - loggingHandler: handler used for logging, + /// - stdoutHandler: asynchronous bidirectional handler closure that receives `stdin` and `stdout` streams as + /// arguments. + /// - stderrHandler: asynchronous unidirectional handler closure that receives `stderr` stream as an argument. + /// - Returns: ``AsyncProcessResult`` value as received from the underlying ``AsyncProcess/waitUntilExit()`` call + /// made on ``AsyncProcess`` instance. + package static func popen( + arguments: [String], + environment: Environment = .current, + loggingHandler: LoggingHandler? = .none, + stdoutHandler: @escaping DuplexStreamHandler, + stderrHandler: ReadableStreamHandler? = nil + ) async throws -> AsyncProcessResult { + let (stdoutStream, stdoutContinuation) = ReadableStream.makeStream() + let (stderrStream, stderrContinuation) = ReadableStream.makeStream() + + let process = AsyncProcess( + arguments: arguments, + environment: environment, + outputRedirection: .stream { + stdoutContinuation.yield($0) + } stderr: { + stderrContinuation.yield($0) + }, + loggingHandler: loggingHandler + ) + + return try await withThrowingTaskGroup(of: Void.self) { group in + let stdinStream = try process.launch() + + group.addTask { + try await stdoutHandler(stdinStream, stdoutStream) + } + + if let stderrHandler { + group.addTask { + try await stderrHandler(stderrStream) + } + } + + defer { + stdoutContinuation.finish() + stderrContinuation.finish() + } + + return try await process.waitUntilExit() + } + } + + /// Execute a subprocess and get its (UTF-8) output if it has a non zero exit. + /// + /// - Parameters: + /// - arguments: The arguments for the subprocess. + /// - environment: The environment to pass to subprocess. By default the current process environment + /// will be inherited. + /// - loggingHandler: Handler for logging messages + /// - Returns: The process output (stdout + stderr). + @discardableResult + package static func checkNonZeroExit( + arguments: [String], + environment: Environment = .current, + loggingHandler: LoggingHandler? = .none + ) async throws -> String { + let result = try await popen( + arguments: arguments, + environment: environment, + loggingHandler: loggingHandler + ) + // Throw if there was a non zero termination. + guard result.exitStatus == .terminated(code: 0) else { + throw AsyncProcessResult.Error.nonZeroExit(result) + } + return try result.utf8Output() + } + + /// Execute a subprocess and get its (UTF-8) output if it has a non zero exit. + /// + /// - Parameters: + /// - args: The arguments for the subprocess. + /// - environment: The environment to pass to subprocess. By default the current process environment + /// will be inherited. + /// - loggingHandler: Handler for logging messages + /// - Returns: The process output (stdout + stderr). + @discardableResult + package static func checkNonZeroExit( + args: String..., + environment: Environment = .current, + loggingHandler: LoggingHandler? = .none + ) async throws -> String { + try await self.checkNonZeroExit( + arguments: args, + environment: environment, + loggingHandler: loggingHandler + ) + } +} + +extension AsyncProcess { + /// Execute a subprocess and calls completion block when it finishes execution + /// + /// - Parameters: + /// - arguments: The arguments for the subprocess. + /// - environment: The environment to pass to subprocess. By default the current process environment + /// will be inherited. + /// - loggingHandler: Handler for logging messages + /// - queue: Queue to use for callbacks + /// - completion: A completion handler to return the process result + @available(*, noasync) + package static func popen( + arguments: [String], + environment: Environment = .current, + loggingHandler: LoggingHandler? = .none, + queue: DispatchQueue? = nil, + completion: @escaping (Result) -> Void + ) { + let completionQueue = queue ?? Self.sharedCompletionQueue + + do { + let process = AsyncProcess( + arguments: arguments, + environment: environment, + outputRedirection: .collect, + loggingHandler: loggingHandler + ) + process.completionQueue = completionQueue + try process.launch() + process.waitUntilExit(completion) + } catch { + completionQueue.async { + completion(.failure(error)) + } + } + } + + /// Execute a subprocess and block until it finishes execution + /// + /// - Parameters: + /// - arguments: The arguments for the subprocess. + /// - environment: The environment to pass to subprocess. By default the current process environment + /// will be inherited. + /// - loggingHandler: Handler for logging messages + /// - Returns: The process result. + @available(*, noasync) + @discardableResult + package static func popen( + arguments: [String], + environment: Environment = .current, + loggingHandler: LoggingHandler? = .none + ) throws -> AsyncProcessResult { + let process = AsyncProcess( + arguments: arguments, + environment: environment, + outputRedirection: .collect, + loggingHandler: loggingHandler + ) + try process.launch() + return try process.waitUntilExit() + } + + /// Execute a subprocess and block until it finishes execution + /// + /// - Parameters: + /// - args: The arguments for the subprocess. + /// - environment: The environment to pass to subprocess. By default the current process environment + /// will be inherited. + /// - loggingHandler: Handler for logging messages + /// - Returns: The process result. + @available(*, noasync) + @discardableResult + package static func popen( + args: String..., + environment: Environment = .current, + loggingHandler: LoggingHandler? = .none + ) throws -> AsyncProcessResult { + try AsyncProcess.popen(arguments: args, environment: environment, loggingHandler: loggingHandler) + } + + /// Execute a subprocess and get its (UTF-8) output if it has a non zero exit. + /// + /// - Parameters: + /// - arguments: The arguments for the subprocess. + /// - environment: The environment to pass to subprocess. By default the current process environment + /// will be inherited. + /// - loggingHandler: Handler for logging messages + /// - Returns: The process output (stdout + stderr). + @available(*, noasync) + @discardableResult + package static func checkNonZeroExit( + arguments: [String], + environment: Environment = .current, + loggingHandler: LoggingHandler? = .none + ) throws -> String { + let process = AsyncProcess( + arguments: arguments, + environment: environment, + outputRedirection: .collect, + loggingHandler: loggingHandler + ) + try process.launch() + let result = try process.waitUntilExit() + // Throw if there was a non zero termination. + guard result.exitStatus == .terminated(code: 0) else { + throw AsyncProcessResult.Error.nonZeroExit(result) + } + return try result.utf8Output() + } + + /// Execute a subprocess and get its (UTF-8) output if it has a non zero exit. + /// + /// - Parameters: + /// - arguments: The arguments for the subprocess. + /// - environment: The environment to pass to subprocess. By default the current process environment + /// will be inherited. + /// - loggingHandler: Handler for logging messages + /// - Returns: The process output (stdout + stderr). + @available(*, noasync) + @discardableResult + package static func checkNonZeroExit( + args: String..., + environment: Environment = .current, + loggingHandler: LoggingHandler? = .none + ) throws -> String { + try self.checkNonZeroExit(arguments: args, environment: environment, loggingHandler: loggingHandler) + } +} + +extension AsyncProcess: Hashable { + package func hash(into hasher: inout Hasher) { + hasher.combine(ObjectIdentifier(self)) + } + + package static func == (lhs: AsyncProcess, rhs: AsyncProcess) -> Bool { + ObjectIdentifier(lhs) == ObjectIdentifier(rhs) + } +} + +// MARK: - Private helpers + +#if !os(Windows) +#if canImport(Darwin) +private typealias swiftpm_posix_spawn_file_actions_t = posix_spawn_file_actions_t? +#else +private typealias swiftpm_posix_spawn_file_actions_t = posix_spawn_file_actions_t +#endif + +private func WIFEXITED(_ status: Int32) -> Bool { + _WSTATUS(status) == 0 +} + +private func _WSTATUS(_ status: Int32) -> Int32 { + status & 0x7F +} + +private func WIFSIGNALED(_ status: Int32) -> Bool { + (_WSTATUS(status) != 0) && (_WSTATUS(status) != 0x7F) +} + +private func WEXITSTATUS(_ status: Int32) -> Int32 { + (status >> 8) & 0xFF +} + +private func WTERMSIG(_ status: Int32) -> Int32 { + status & 0x7F +} + +/// Open the given pipe. +private func open(pipe buffer: inout [Int32]) throws { + let rv = pipe(&buffer) + guard rv == 0 else { + throw SystemError.pipe(rv) + } +} + +/// Close the given fd. +private func close(fd: Int32) throws { + func innerClose(_ fd: inout Int32) throws { + let rv = close(fd) + guard rv == 0 else { + throw SystemError.close(rv) + } + } + var innerFd = fd + try innerClose(&innerFd) +} + +extension AsyncProcess.Error: CustomStringConvertible { + package var description: String { + switch self { + case .missingExecutableProgram(let program): + "could not find executable for '\(program)'" + case .workingDirectoryNotSupported: + "workingDirectory is not supported in this platform" + case .stdinUnavailable: + "could not open stdin on this platform" + } + } +} + +extension AsyncProcess.Error: CustomNSError { + package var errorUserInfo: [String: Any] { + [NSLocalizedDescriptionKey: self.description] + } +} + +#endif + +extension AsyncProcessResult.Error: CustomStringConvertible { + package var description: String { + switch self { + case .systemError(let arguments, let underlyingError): + return "error while executing `\(arguments.joined(separator: " "))`: \(underlyingError)" + case .illegalUTF8Sequence: + return "illegal UTF8 sequence output" + case .nonZeroExit(let result): + var str = "" + switch result.exitStatus { + case .terminated(let code): + str.append(contentsOf: "terminated(\(code)): ") + #if os(Windows) + case .abnormal(let exception): + str.append(contentsOf: "abnormal(\(exception)): ") + #else + case .signalled(let signal): + str.append(contentsOf: "signalled(\(signal)): ") + #endif + } + + // Strip sandbox information from arguments to keep things pretty. + var args = result.arguments + // This seems a little fragile. + if args.first == "sandbox-exec", args.count > 3 { + args = args.suffix(from: 3).map { $0 } + } + str.append(contentsOf: args.map { $0.spm_shellEscaped() }.joined(separator: " ")) + + // Include the output, if present. + if let output = try? result.utf8Output() + result.utf8stderrOutput() { + // We indent the output to keep it visually separated from everything else. + let indentation = " " + str.append(contentsOf: " output:\n") + str.append(contentsOf: indentation) + str.append(contentsOf: output.split(whereSeparator: { $0.isNewline }) + .joined(separator: "\n\(indentation)")) + if !output.hasSuffix("\n") { + str.append(contentsOf: "\n") + } + } + + return str + } + } +} + +#if os(Windows) +extension FileHandle: WritableByteStream { + package var position: Int { + Int(offsetInFile) + } + + package func write(_ byte: UInt8) { + self.write(Data([byte])) + } + + package func write(_ bytes: some Collection) { + self.write(Data(bytes)) + } + + package func flush() { + synchronizeFile() + } +} +#endif diff --git a/Sources/Basics/Concurrency/ConcurrencyHelpers.swift b/Sources/Basics/Concurrency/ConcurrencyHelpers.swift index c8160ccb408..2837bea4b95 100644 --- a/Sources/Basics/Concurrency/ConcurrencyHelpers.swift +++ b/Sources/Basics/Concurrency/ConcurrencyHelpers.swift @@ -15,30 +15,31 @@ import Dispatch import class Foundation.NSLock import class Foundation.ProcessInfo import struct Foundation.URL -import enum TSCBasic.ProcessEnv +import struct Foundation.UUID import func TSCBasic.tsc_await public enum Concurrency { public static var maxOperations: Int { - ProcessEnv.vars["SWIFTPM_MAX_CONCURRENT_OPERATIONS"].flatMap(Int.init) ?? ProcessInfo.processInfo + Environment.current["SWIFTPM_MAX_CONCURRENT_OPERATIONS"].flatMap(Int.init) ?? ProcessInfo.processInfo .activeProcessorCount } } -// FIXME: mark as deprecated once async/await is available -// @available(*, deprecated, message: "replace with async/await when available") -@inlinable -public func temp_await(_ body: (@escaping (Result) -> Void) -> Void) throws -> T { - try tsc_await(body) -} +@available(*, noasync, message: "This method blocks the current thread indefinitely. Calling it from the concurrency pool can cause deadlocks") +public func unsafe_await(_ body: @Sendable @escaping () async -> T) -> T { + let semaphore = DispatchSemaphore(value: 0) -// FIXME: mark as deprecated once async/await is available -// @available(*, deprecated, message: "replace with async/await when available") -@inlinable -public func temp_await(_ body: (@escaping (T) -> Void) -> Void) -> T { - tsc_await(body) + let box = ThreadSafeBox() + Task { + let localValue: T = await body() + box.mutate { _ in localValue } + semaphore.signal() + } + semaphore.wait() + return box.get()! } + extension DispatchQueue { // a shared concurrent queue for running concurrent asynchronous operations public static let sharedConcurrent = DispatchQueue( @@ -47,37 +48,232 @@ extension DispatchQueue { ) } -/// Bridges between potentially blocking methods that take a result completion closure and async/await -public func safe_async( - _ body: @Sendable @escaping (@Sendable @escaping (Result) -> Void) -> Void -) async throws -> T { - try await withCheckedThrowingContinuation { continuation in - // It is possible that body make block indefinitely on a lock, semaphore, - // or similar then synchronously call the completion handler. For full safety - // it is essential to move the execution off the swift concurrency pool - DispatchQueue.sharedConcurrent.async { - body { - continuation.resume(with: $0) +extension DispatchQueue { + package func scheduleOnQueue(work: @escaping @Sendable () throws -> T) async throws -> T { + try await withCheckedThrowingContinuation { continuation in + self.async { + do { + continuation.resume(returning: try work()) + } catch { + continuation.resume(throwing: error) + } } } } -} -/// Bridges between potentially blocking methods that take a result completion closure and async/await -public func safe_async(_ body: @escaping (@escaping (Result) -> Void) -> Void) async -> T { - await withCheckedContinuation { continuation in - // It is possible that body make block indefinitely on a lock, sempahore, - // or similar then synchrously call the completion handler. For full safety - // it is essential to move the execution off the swift concurrency pool - DispatchQueue.sharedConcurrent.async { - body { - continuation.resume(with: $0) + package func asyncResult(_ callback: @escaping @Sendable (Result) -> Void, _ closure: @escaping @Sendable () async throws -> T) { + let completion: @Sendable (Result) -> Void = { + result in self.async { + callback(result) + } + } + + Task { + do { + completion(.success(try await closure())) + } catch { + completion(.failure(error)) } } } } -#if !canImport(Darwin) -// As of Swift 5.7 and 5.8 swift-corelibs-foundation doesn't have `Sendable` annotations yet. -extension URL: @unchecked Sendable {} -#endif +/// A queue for running async operations with a limit on the number of concurrent tasks. +public final class AsyncOperationQueue: @unchecked Sendable { + + // This implementation is identical to the AsyncOperationQueue in swift-build. + // Any modifications made here should also be made there. + // https://github.com/swiftlang/swift-build/blob/main/Sources/SWBUtil/AsyncOperationQueue.swift#L13 + + fileprivate typealias ID = UUID + fileprivate typealias WaitingContinuation = CheckedContinuation + + private let concurrentTasks: Int + private var waitingTasks: [WorkTask] = [] + private let waitingTasksLock = NSLock() + + fileprivate enum WorkTask { + case creating(ID) + case waiting(ID, WaitingContinuation) + case running(ID) + case cancelled(ID) + + var id: ID { + switch self { + case .creating(let id), .waiting(let id, _), .running(let id), .cancelled(let id): + return id + } + } + + var continuation: WaitingContinuation? { + guard case .waiting(_, let continuation) = self else { + return nil + } + return continuation + } + } + + /// Creates an `AsyncOperationQueue` with a specified number of concurrent tasks. + /// - Parameter concurrentTasks: The maximum number of concurrent tasks that can be executed concurrently. + public init(concurrentTasks: Int) { + self.concurrentTasks = concurrentTasks + } + + deinit { + waitingTasksLock.withLock { + if !waitingTasks.isEmpty { + preconditionFailure("Deallocated with waiting tasks") + } + } + } + + /// Executes an asynchronous operation, ensuring that the number of concurrent tasks + // does not exceed the specified limit. + /// - Parameter operation: The asynchronous operation to execute. + /// - Returns: The result of the operation. + /// - Throws: An error thrown by the operation, or a `CancellationError` if the operation is cancelled. + public func withOperation( + _ operation: () async throws -> sending ReturnValue + ) async throws -> ReturnValue { + let taskId = try await waitIfNeeded() + defer { signalCompletion(taskId) } + return try await operation() + } + + private func waitIfNeeded() async throws -> ID { + let workTask = waitingTasksLock.withLock({ + let shouldWait = waitingTasks.count >= concurrentTasks + let workTask = shouldWait ? WorkTask.creating(ID()) : .running(ID()) + waitingTasks.append(workTask) + return workTask + }) + + // If we aren't creating a task that needs to wait, we're under the concurrency limit. + guard case .creating(let taskId) = workTask else { + return workTask.id + } + + enum TaskAction { + case start(WaitingContinuation) + case cancel(WaitingContinuation) + } + + try await withTaskCancellationHandler { + try await withCheckedThrowingContinuation { (continuation: WaitingContinuation) -> Void in + let action: TaskAction? = waitingTasksLock.withLock { + guard let index = waitingTasks.firstIndex(where: { $0.id == taskId }) else { + // The task may have been marked as cancelled already and then removed from + // waitingTasks in `signalCompletion`. + return .cancel(continuation) + } + + switch waitingTasks[index] { + case .cancelled: + // If the task was cancelled in between creating the task cancellation handler and acquiring the lock, + // we should resume the continuation with a `CancellationError`. + waitingTasks.remove(at: index) + return .cancel(continuation) + case .creating, .running, .waiting: + // A task may have completed since we initially checked if we should wait. Check again in this locked + // section and if we can start it, remove it from the waiting tasks and start it immediately. + if waitingTasks.count >= concurrentTasks { + waitingTasks[index] = .waiting(taskId, continuation) + return nil + } else { + waitingTasks.remove(at: index) + return .start(continuation) + } + } + } + + switch action { + case .some(.cancel(let continuation)): + continuation.resume(throwing: _Concurrency.CancellationError()) + case .some(.start(let continuation)): + continuation.resume() + case .none: + return + } + } + } onCancel: { + let continuation: WaitingContinuation? = self.waitingTasksLock.withLock { + guard let taskIndex = self.waitingTasks.firstIndex(where: { $0.id == taskId }) else { + return nil + } + + switch self.waitingTasks[taskIndex] { + case .waiting(_, let continuation): + self.waitingTasks.remove(at: taskIndex) + + // If the parent task is cancelled then we need to manually handle resuming the + // continuation for the waiting task with a `CancellationError`. Return the continuation + // here so it can be resumed once the `waitingTasksLock` is released. + return continuation + case .creating, .running: + // If the task was still being created, mark it as cancelled in `waitingTasks` so that + // the handler for `withCheckedThrowingContinuation` can immediately cancel it. + self.waitingTasks[taskIndex] = .cancelled(taskId) + return nil + case .cancelled: + preconditionFailure("Attempting to cancel a task that was already cancelled") + } + } + + continuation?.resume(throwing: _Concurrency.CancellationError()) + } + return workTask.id + } + + private func signalCompletion(_ taskId: ID) { + let continuationToResume = waitingTasksLock.withLock { () -> WaitingContinuation? in + guard !waitingTasks.isEmpty else { + return nil + } + + // Remove the completed task from the list to decrement the active task count. + if let taskIndex = self.waitingTasks.firstIndex(where: { $0.id == taskId }) { + waitingTasks.remove(at: taskIndex) + } + + // We cannot remove elements from `waitingTasks` while iterating over it, so we make + // a pass to collect operations and then apply them after the loop. + func createTaskListOperations() -> (CollectionDifference?, WaitingContinuation?) { + var changes: [CollectionDifference.Change] = [] + for (index, task) in waitingTasks.enumerated() { + switch task { + case .running: + // Skip tasks that are already running, looking for the first one that is waiting or creating. + continue + case .creating: + // If the next task is in the process of being created, let the + // creation code in the `withCheckedThrowingContinuation` in `waitIfNeeded` + // handle starting the task. + break + case .waiting: + // Begin the next waiting task + changes.append(.remove(offset: index, element: task, associatedWith: nil)) + return (CollectionDifference(changes), task.continuation) + case .cancelled: + // If the next task is cancelled, continue removing cancelled + // tasks until we find one that hasn't run yet, or we exaust the list of waiting tasks. + changes.append(.remove(offset: index, element: task, associatedWith: nil)) + continue + } + } + return (CollectionDifference(changes), nil) + } + + let (collectionOperations, continuation) = createTaskListOperations() + if let operations = collectionOperations { + guard let appliedDiff = waitingTasks.applying(operations) else { + preconditionFailure("Failed to apply changes to waiting tasks") + } + waitingTasks = appliedDiff + } + + return continuation + } + + continuationToResume?.resume() + } +} diff --git a/Sources/Basics/Concurrency/SendableBox.swift b/Sources/Basics/Concurrency/SendableBox.swift index 0af62417dbb..a5ec3673be0 100644 --- a/Sources/Basics/Concurrency/SendableBox.swift +++ b/Sources/Basics/Concurrency/SendableBox.swift @@ -16,24 +16,24 @@ import struct Foundation.Date /// an `async` closure. This type serves as a replacement for `ThreadSafeBox` /// implemented with Swift Concurrency primitives. public actor SendableBox { - init(_ value: Value? = nil) { + public init(_ value: Value) { self.value = value } - var value: Value? + public var value: Value + + public func set(_ value: Value) { + self.value = value + } } extension SendableBox where Value == Int { func increment() { - if let value { - self.value = value + 1 - } + self.value = value + 1 } func decrement() { - if let value { - self.value = value - 1 - } + self.value = value - 1 } } diff --git a/Sources/Basics/Concurrency/ThreadSafeBox.swift b/Sources/Basics/Concurrency/ThreadSafeBox.swift index 75b9a1542a9..c05347a46ad 100644 --- a/Sources/Basics/Concurrency/ThreadSafeBox.swift +++ b/Sources/Basics/Concurrency/ThreadSafeBox.swift @@ -30,6 +30,12 @@ public final class ThreadSafeBox { self.underlying = value } } + + public func mutate(body: (inout Value?) throws -> ()) rethrows { + try self.lock.withLock { + try body(&self.underlying) + } + } @discardableResult public func memoize(body: () throws -> Value) rethrows -> Value { @@ -43,6 +49,18 @@ public final class ThreadSafeBox { return value } + @discardableResult + public func memoize(body: () async throws -> Value) async rethrows -> Value { + if let value = self.get() { + return value + } + let value = try await body() + self.lock.withLock { + self.underlying = value + } + return value + } + public func clear() { self.lock.withLock { self.underlying = nil diff --git a/Sources/Basics/Concurrency/ThreadSafeKeyValueStore.swift b/Sources/Basics/Concurrency/ThreadSafeKeyValueStore.swift index 4a700df9d6d..c3bafbd1ea8 100644 --- a/Sources/Basics/Concurrency/ThreadSafeKeyValueStore.swift +++ b/Sources/Basics/Concurrency/ThreadSafeKeyValueStore.swift @@ -10,8 +10,88 @@ // //===----------------------------------------------------------------------===// +import _Concurrency + import class Foundation.NSLock +/// Thread-safe dictionary with async memoization +public actor ThrowingAsyncKeyValueMemoizer { + var stored: [Key: Task] = [:] + + public init() { + self.stored = [:] + } + + public func memoize(_ key: Key, body: @Sendable @escaping () async throws -> Value) async throws -> Value { + guard let existingTask = self.stored[key] else { + let newTask = Task { + try await body() + } + self.stored[key] = newTask + return try await newTask.value + } + return try await existingTask.value + } +} + +public actor AsyncKeyValueMemoizer { + var stored: [Key: Task] = [:] + + public init() { + self.stored = [:] + } + + public func memoize(_ key: Key, body: @Sendable @escaping () async -> Value) async -> Value { + guard let existingTask = self.stored[key] else { + let newTask = Task { + await body() + } + self.stored[key] = newTask + return await newTask.value + } + return await existingTask.value + } +} + +public actor AsyncThrowingValueMemoizer { + var stored: ValueStorage? + + enum ValueStorage { + case inProgress([CheckedContinuation]) + case complete(Result) + } + + public init() {} + + public func memoize(body: @Sendable () async throws -> Value) async throws -> Value { + guard let stored else { + self.stored = .inProgress([]) + let result: Result + do { + result = try await .success(body()) + } catch { + result = .failure(error) + } + if case .inProgress(let array) = self.stored { + self.stored = .complete(result) + array.forEach { $0.resume(with: result)} + } + return try result.get() + } + switch stored { + + case .inProgress(let existing): + return try await withCheckedThrowingContinuation { + self.stored = .inProgress(existing + [$0]) + } + case .complete(let result): + return try result.get() + } + } +} + + + /// Thread-safe dictionary like structure public final class ThreadSafeKeyValueStore where Key: Hashable { private var underlying: [Key: Value] diff --git a/Sources/Basics/Concurrency/ThrowingDefer.swift b/Sources/Basics/Concurrency/ThrowingDefer.swift new file mode 100644 index 00000000000..fdf821f7c96 --- /dev/null +++ b/Sources/Basics/Concurrency/ThrowingDefer.swift @@ -0,0 +1,59 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2023-2024 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +/// Runs a cleanup closure (`deferred`) after a given `work` closure, +/// making sure `deferred` is run also when `work` throws an error. +/// - Parameters: +/// - work: The work that should be performed. Will always be executed. +/// - deferred: The cleanup that needs to be done in any case. +/// - Throws: Any error thrown by `deferred` or `work` (in that order). +/// - Returns: The result of `work`. +/// - Note: If `work` **and** `deferred` throw an error, +/// the one thrown by `deferred` is thrown from this function. +/// - SeeAlso: ``withAsyncThrowing(do:defer:)`` +public func withThrowing( + do work: () throws -> T, + defer deferred: () throws -> Void +) throws -> T { + do { + let result = try work() + try deferred() + return result + } catch { + try deferred() + throw error + } +} + +/// Runs an async cleanup closure (`deferred`) after a given async `work` closure, +/// making sure `deferred` is run also when `work` throws an error. +/// - Parameters: +/// - work: The work that should be performed. Will always be executed. +/// - deferred: The cleanup that needs to be done in any case. +/// - Throws: Any error thrown by `deferred` or `work` (in that order). +/// - Returns: The result of `work`. +/// - Note: If `work` **and** `deferred` throw an error, +/// the one thrown by `deferred` is thrown from this function. +/// - SeeAlso: ``withThrowing(do:defer:)`` +public func withAsyncThrowing( + do work: @Sendable () async throws -> T, + defer deferred: @Sendable () async throws -> Void +) async throws -> T { + do { + let result = try await work() + try await deferred() + return result + } catch { + try await deferred() + throw error + } +} diff --git a/Sources/Basics/Concurrency/TokenBucket.swift b/Sources/Basics/Concurrency/TokenBucket.swift index e9cf6ff4251..8c7b8992b63 100644 --- a/Sources/Basics/Concurrency/TokenBucket.swift +++ b/Sources/Basics/Concurrency/TokenBucket.swift @@ -11,7 +11,7 @@ //===----------------------------------------------------------------------===// import _Concurrency -import DequeModule +private import DequeModule /// Type modeled after a "token bucket" pattern, which is similar to a semaphore, but is built with /// Swift Concurrency primitives. @@ -30,7 +30,7 @@ public actor TokenBucket { /// invocations of `withToken` will suspend until a "free" token is available. /// - Parameter body: The closure to invoke when a token is available. /// - Returns: Resulting value returned by `body`. - public func withToken( + public func withToken( _ body: @Sendable () async throws -> ReturnType ) async rethrows -> ReturnType { await self.getToken() diff --git a/Sources/Basics/DispatchTimeInterval+Extensions.swift b/Sources/Basics/DispatchTimeInterval+Extensions.swift index 2f37d472994..4c74b3bc61e 100644 --- a/Sources/Basics/DispatchTimeInterval+Extensions.swift +++ b/Sources/Basics/DispatchTimeInterval+Extensions.swift @@ -86,7 +86,7 @@ extension DispatchTimeInterval { return String(format: "%.2f", Double(value) / Double(1_000_000_000)) + "s" case .never: return "n/a" - #if os(macOS) || os(iOS) || os(tvOS) || os(watchOS) + #if canImport(Darwin) @unknown default: return "n/a" #endif @@ -95,7 +95,7 @@ extension DispatchTimeInterval { } // remove when available to all platforms -#if os(Linux) || os(Windows) || os(Android) || os(OpenBSD) +#if os(Linux) || os(Windows) || os(Android) || os(OpenBSD) || os(FreeBSD) extension DispatchTime { public func distance(to: DispatchTime) -> DispatchTimeInterval { let final = to.uptimeNanoseconds diff --git a/Sources/Basics/Environment/Environment.swift b/Sources/Basics/Environment/Environment.swift new file mode 100644 index 00000000000..59650363155 --- /dev/null +++ b/Sources/Basics/Environment/Environment.swift @@ -0,0 +1,361 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2024 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 Foundation + +#if canImport(Glibc) +import Glibc +#elseif canImport(Musl) +import Musl +#elseif os(Windows) +import CRT +import WinSDK +#elseif canImport(Android) +import Android +#else +import Darwin.C +#endif + +// FIXME: Use Synchronization.Mutex when available +private final class Mutex: @unchecked Sendable { + var lock: NSLock + var value: T + + init(value: T) { + self.lock = .init() + self.value = value + } + + func withLock(_ body: (inout T) -> U) -> U { + self.lock.lock() + defer { self.lock.unlock() } + return body(&self.value) + } +} + +// FIXME: This should come from Foundation +// FIXME: package (public required by users) +public struct Environment { + var storage: [EnvironmentKey: String] +} + +// MARK: - Accessors + +extension Environment { + package init() { + self.storage = .init() + } + + package subscript(_ key: EnvironmentKey) -> String? { + _read { yield self.storage[key] } + _modify { yield &self.storage[key] } + } +} + +// MARK: - Conversions between Dictionary + +extension Environment { + @_spi(SwiftPMInternal) + public init(_ dictionary: [String: String]) { + self.storage = .init() + let sorted = dictionary.sorted { $0.key < $1.key } + for (key, value) in sorted { + self.storage[.init(key)] = value + } + } +} + +extension [String: String] { + @_spi(SwiftPMInternal) + public init(_ environment: Environment) { + self.init() + let sorted = environment.sorted { $0.key < $1.key } + for (key, value) in sorted { + self[key.rawValue] = value + } + } +} + +// MARK: - Path Modification + +extension Environment { + package mutating func prependPath(key: EnvironmentKey, value: String) { + guard !value.isEmpty else { return } + if let existing = self[key] { + self[key] = "\(value)\(Self.pathEntryDelimiter)\(existing)" + } else { + self[key] = value + } + } + + package mutating func appendPath(key: EnvironmentKey, value: String) { + guard !value.isEmpty else { return } + if let existing = self[key] { + self[key] = "\(existing)\(Self.pathEntryDelimiter)\(value)" + } else { + self[key] = value + } + } + + package static var pathEntryDelimiter: String { + #if os(Windows) + ";" + #else + ":" + #endif + } +} + +// MARK: - Global Environment + +extension Environment { + fileprivate static let _cachedCurrent = Mutex(value: nil) + + /// Vends a copy of the current process's environment variables. + /// + /// Mutations to the current process's global environment are not reflected + /// in the returned value. + public static var current: Self { + Self._cachedCurrent.withLock { cachedValue in + if let cachedValue = cachedValue { + return cachedValue + } else { + let current = Self(ProcessInfo.processInfo.environment) + cachedValue = current + return current + } + } + } + + /// Temporary override environment variables + /// + /// WARNING! This method is not thread-safe. POSIX environments are shared + /// between threads. This means that when this method is called simultaneously + /// from different threads, the environment will neither be setup nor restored + /// correctly. + package static func makeCustom( + _ environment: Self, + body: () async throws -> T + ) async throws -> T { + let current = Self.current + let state = environment.storage.keys.map { ($0, current[$0]) } + let restore = { + for (key, value) in state { + try Self.set(key: key, value: value) + } + } + let returnValue: T + do { + for (key, value) in environment { + try Self.set(key: key, value: value) + } + returnValue = try await body() + } catch { + try? restore() + throw error + } + try restore() + return returnValue + } + + /// Temporary override environment variables + /// + /// WARNING! This method is not thread-safe. POSIX environments are shared + /// between threads. This means that when this method is called simultaneously + /// from different threads, the environment will neither be setup nor restored + /// correctly. + package static func makeCustom( + _ environment: Self, + body: () throws -> T + ) throws -> T { + let current = Self.current + let state = environment.storage.keys.map { ($0, current[$0]) } + let restore = { + for (key, value) in state { + try Self.set(key: key, value: value) + } + } + let returnValue: T + do { + for (key, value) in environment { + try Self.set(key: key, value: value) + } + returnValue = try body() + } catch { + try? restore() + throw error + } + try restore() + return returnValue + } + + struct UpdateEnvironmentError: CustomStringConvertible, Error { + var function: StaticString + var code: Int32 + var description: String { "\(self.function) returned \(self.code)" } + } + + /// Modifies the process's global environment. + /// + /// > Important: This operation is _not_ concurrency safe. + package static func set(key: EnvironmentKey, value: String?) throws { + #if os(Windows) + func _SetEnvironmentVariableW(_ key: String, _ value: String?) -> Bool { + key.withCString(encodedAs: UTF16.self) { key in + if let value { + value.withCString(encodedAs: UTF16.self) { value in + SetEnvironmentVariableW(key, value) + } + } else { + SetEnvironmentVariableW(key, nil) + } + } + } + #endif + + // Invalidate cached value after mutating the global environment. + // This is potentially overly safe because we may not need to invalidate + // the cache if the mutation fails. However this approach is easier to + // read and reason about. + defer { Self._cachedCurrent.withLock { $0 = nil } } + if let value = value { + #if os(Windows) + guard _SetEnvironmentVariableW(key.rawValue, value) else { + throw UpdateEnvironmentError( + function: "SetEnvironmentVariableW", + code: Int32(GetLastError()) + ) + } + guard _putenv("\(key)=\(value)") == 0 else { + throw UpdateEnvironmentError( + function: "_putenv", + code: Int32(GetLastError()) + ) + } + #else + guard setenv(key.rawValue, value, 1) == 0 else { + throw UpdateEnvironmentError( + function: "setenv", + code: errno + ) + } + #endif + } else { + #if os(Windows) + guard _SetEnvironmentVariableW(key.rawValue, nil) else { + throw UpdateEnvironmentError( + function: "SetEnvironmentVariableW", + code: Int32(GetLastError()) + ) + } + guard _putenv("\(key)=") == 0 else { + throw UpdateEnvironmentError( + function: "_putenv", + code: Int32(GetLastError()) + ) + } + #else + guard unsetenv(key.rawValue) == 0 else { + throw UpdateEnvironmentError( + function: "unsetenv", + code: errno + ) + } + #endif + } + } +} + +// MARK: - Cachable Keys + +extension Environment { + /// Returns a copy of `self` with known non-cacheable keys removed. + /// + /// - Issue: rdar://107029374 + package var cachable: Environment { + var cachable = Environment() + for (key, value) in self { + if !EnvironmentKey.nonCachable.contains(key) { + cachable[key] = value + } + } + return cachable + } +} + +// MARK: - Protocol Conformances + +extension Environment: Collection { + public struct Index: Comparable { + public static func < (lhs: Self, rhs: Self) -> Bool { + lhs.underlying < rhs.underlying + } + + var underlying: Dictionary.Index + } + + public typealias Element = (key: EnvironmentKey, value: String) + + public var startIndex: Index { + Index(underlying: self.storage.startIndex) + } + + public var endIndex: Index { + Index(underlying: self.storage.endIndex) + } + + public subscript(index: Index) -> Element { + self.storage[index.underlying] + } + + public func index(after index: Self.Index) -> Self.Index { + Index(underlying: self.storage.index(after: index.underlying)) + } +} + +extension Environment: CustomStringConvertible { + public var description: String { + let body = self + .sorted { $0.key < $1.key } + .map { "\"\($0.rawValue)=\($1)\"" } + .joined(separator: ", ") + return "[\(body)]" + } +} + +extension Environment: Encodable { + public func encode(to encoder: any Encoder) throws { + try self.storage.encode(to: encoder) + } +} + +extension Environment: Equatable {} + +extension Environment: ExpressibleByDictionaryLiteral { + public typealias Key = EnvironmentKey + public typealias Value = String + + public init(dictionaryLiteral elements: (Key, Value)...) { + self.storage = .init() + for (key, value) in elements { + self.storage[key] = value + } + } +} + +extension Environment: Decodable { + public init(from decoder: any Decoder) throws { + self.storage = try .init(from: decoder) + } +} + +extension Environment: Sendable {} diff --git a/Sources/Basics/Environment/EnvironmentKey.swift b/Sources/Basics/Environment/EnvironmentKey.swift new file mode 100644 index 00000000000..8d31fcc4ab7 --- /dev/null +++ b/Sources/Basics/Environment/EnvironmentKey.swift @@ -0,0 +1,114 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2024 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 +// +//===----------------------------------------------------------------------===// + +/// A key used to access values in an ``Environment``. +/// +/// This type respects the compiled platform's case sensitivity requirements. +public struct EnvironmentKey { + public var rawValue: String + + package init(_ rawValue: String) { + self.rawValue = rawValue + } +} + +extension EnvironmentKey { + package static let path: Self = "PATH" + + package static var libraryPath: Self { + #if os(Windows) + path + #elseif canImport(Darwin) + "DYLD_LIBRARY_PATH" + #else + "LD_LIBRARY_PATH" + #endif + } + + /// A set of known keys which should not be included in cache keys. + package static let nonCachable: Set = [ + "TERM", + "TERM_PROGRAM", + "TERM_PROGRAM_VERSION", + "TERM_SESSION_ID", + "ITERM_PROFILE", + "ITERM_SESSION_ID", + "SECURITYSESSIONID", + "LaunchInstanceID", + "LC_TERMINAL", + "LC_TERMINAL_VERSION", + "CLICOLOR", + "LS_COLORS", + "VSCODE_IPC_HOOK_CLI", + "HYPERFINE_RANDOMIZED_ENVIRONMENT_OFFSET", + "SSH_AUTH_SOCK", + ] +} + +extension EnvironmentKey: CodingKeyRepresentable {} + +extension EnvironmentKey: Comparable { + public static func < (lhs: Self, rhs: Self) -> Bool { + // Even on windows use a stable sort order. + lhs.rawValue < rhs.rawValue + } +} + +extension EnvironmentKey: CustomStringConvertible { + public var description: String { self.rawValue } +} + +extension EnvironmentKey: Encodable { + public func encode(to encoder: any Encoder) throws { + try self.rawValue.encode(to: encoder) + } +} + +extension EnvironmentKey: Equatable { + public static func == (_ lhs: Self, _ rhs: Self) -> Bool { + #if os(Windows) + lhs.rawValue.lowercased() == rhs.rawValue.lowercased() + #else + lhs.rawValue == rhs.rawValue + #endif + } +} + +extension EnvironmentKey: ExpressibleByStringLiteral { + public init(stringLiteral rawValue: String) { + self.init(rawValue) + } +} + +extension EnvironmentKey: Decodable { + public init(from decoder: any Decoder) throws { + self.rawValue = try String(from: decoder) + } +} + +extension EnvironmentKey: Hashable { + public func hash(into hasher: inout Hasher) { + #if os(Windows) + self.rawValue.lowercased().hash(into: &hasher) + #else + self.rawValue.hash(into: &hasher) + #endif + } +} + +extension EnvironmentKey: RawRepresentable { + public init?(rawValue: String) { + self.rawValue = rawValue + } +} + +extension EnvironmentKey: Sendable {} diff --git a/Sources/Basics/Environment/EnvironmentShims.swift b/Sources/Basics/Environment/EnvironmentShims.swift new file mode 100644 index 00000000000..bcf90f62eab --- /dev/null +++ b/Sources/Basics/Environment/EnvironmentShims.swift @@ -0,0 +1,37 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2021 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 struct TSCBasic.ProcessEnvironmentBlock + +import Dispatch + +// FIXME: remove ProcessEnvironmentBlockShims +// only needed outside this module for Git +extension Environment { + @_spi(ProcessEnvironmentBlockShim) + public init(_ processEnvironmentBlock: ProcessEnvironmentBlock) { + self.init() + for (key, value) in processEnvironmentBlock { + self[.init(key.value)] = value + } + } +} + +extension ProcessEnvironmentBlock { + @_spi(ProcessEnvironmentBlockShim) + public init(_ environment: Environment) { + self.init() + for (key, value) in environment { + self[.init(key.rawValue)] = value + } + } +} diff --git a/Sources/Basics/EnvironmentVariables.swift b/Sources/Basics/EnvironmentVariables.swift deleted file mode 100644 index 25e89781a0f..00000000000 --- a/Sources/Basics/EnvironmentVariables.swift +++ /dev/null @@ -1,87 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift open source project -// -// Copyright (c) 2021 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 class Foundation.ProcessInfo - -public typealias EnvironmentVariables = [String: String] - -extension EnvironmentVariables { - public static func empty() -> EnvironmentVariables { - [:] - } - - public static func process() -> EnvironmentVariables { - ProcessInfo.processInfo.environment - } - - public mutating func prependPath(_ key: String, value: String) { - var values = value.isEmpty ? [] : [value] - if let existing = self[key], !existing.isEmpty { - values.append(existing) - } - self.setPath(key, values) - } - - public mutating func appendPath(_ key: String, value: String) { - var values = value.isEmpty ? [] : [value] - if let existing = self[key], !existing.isEmpty { - values.insert(existing, at: 0) - } - self.setPath(key, values) - } - - private mutating func setPath(_ key: String, _ values: [String]) { - #if os(Windows) - let delimiter = ";" - #else - let delimiter = ":" - #endif - self[key] = values.joined(separator: delimiter) - } - - /// `PATH` variable in the process's environment (`Path` under Windows). - public var path: String? { - #if os(Windows) - let pathArg = "Path" - #else - let pathArg = "PATH" - #endif - return self[pathArg] - } -} - -// filter env variable that should not be included in a cache as they change -// often and should not be considered in business logic -// rdar://107029374 -extension EnvironmentVariables { - // internal for testing - internal static let nonCachableKeys: Set = [ - "TERM", - "TERM_PROGRAM", - "TERM_PROGRAM_VERSION", - "TERM_SESSION_ID", - "ITERM_PROFILE", - "ITERM_SESSION_ID", - "SECURITYSESSIONID", - "LaunchInstanceID", - "LC_TERMINAL", - "LC_TERMINAL_VERSION", - "CLICOLOR", - "LS_COLORS", - "VSCODE_IPC_HOOK_CLI", - "HYPERFINE_RANDOMIZED_ENVIRONMENT_OFFSET", - ] - - public var cachable: EnvironmentVariables { - return self.filter { !Self.nonCachableKeys.contains($0.key) } - } -} diff --git a/Sources/Basics/Errors.swift b/Sources/Basics/Errors.swift index c0900f565d7..d8a0f80f228 100644 --- a/Sources/Basics/Errors.swift +++ b/Sources/Basics/Errors.swift @@ -21,9 +21,28 @@ public struct InternalError: Error { private let description: String public init(_ description: String) { assertionFailure(description) - self - .description = - "Internal error. Please file a bug at https://github.com/apple/swift-package-manager/issues with this info. \(description)" + self.description = + "Internal error. Please file a bug at https://github.com/swiftlang/swift-package-manager/issues with this info. \(description)" + } +} + +/// Wraps another error and provides additional context when printed. +/// This is useful for user facing errors that need to provide a user friendly message +/// explaning why an error might have occured, while still showing the detailed underlying error. +public struct ErrorWithContext: Error { + public let error: E + public let context: String + public init(_ error: E, _ context: String) { + self.error = error + self.context = context + } +} + +extension ErrorWithContext: LocalizedError { + public var errorDescription: String? { + return (context.split(separator: "\n") + [error.interpolationDescription]) + .map { "\t\($0)" } + .joined(separator: "\n") } } diff --git a/Sources/Basics/FileSystem/AbsolutePath.swift b/Sources/Basics/FileSystem/AbsolutePath.swift index 4f4034d08f3..c472e5c0608 100644 --- a/Sources/Basics/FileSystem/AbsolutePath.swift +++ b/Sources/Basics/FileSystem/AbsolutePath.swift @@ -321,7 +321,7 @@ extension AbsolutePath { extension AbsolutePath { public var escapedPathString: String { - self.pathString.replacingOccurrences(of: "\\", with: "\\\\") + self.pathString.replacing("\\", with: "\\\\") } } diff --git a/Sources/Basics/FileSystem/FileSystem+Extensions.swift b/Sources/Basics/FileSystem/FileSystem+Extensions.swift index c03178d3428..5f9674b2ef5 100644 --- a/Sources/Basics/FileSystem/FileSystem+Extensions.swift +++ b/Sources/Basics/FileSystem/FileSystem+Extensions.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift open source project // -// Copyright (c) 2020-2021 Apple Inc. and the Swift project authors +// Copyright (c) 2020-2024 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 @@ -13,7 +13,6 @@ import struct Foundation.Data import class Foundation.FileManager import struct Foundation.UUID -import SystemPackage import struct TSCBasic.ByteString import struct TSCBasic.FileInfo @@ -25,7 +24,7 @@ import var TSCBasic.localFileSystem import protocol TSCBasic.WritableByteStream public typealias FileSystem = TSCBasic.FileSystem -public var localFileSystem = TSCBasic.localFileSystem +public let localFileSystem = TSCBasic.localFileSystem // MARK: - Custom path @@ -196,8 +195,13 @@ extension FileSystem { } /// Execute the given block while holding the lock. - public func withLock(on path: AbsolutePath, type: FileLock.LockType, _ body: () throws -> T) throws -> T { - try self.withLock(on: path.underlying, type: type, body) + public func withLock(on path: AbsolutePath, type: FileLock.LockType, blocking: Bool = true, _ body: () throws -> T) throws -> T { + try self.withLock(on: path.underlying, type: type, blocking: blocking, body) + } + + /// Execute the given block while holding the lock. + public func withLock(on path: AbsolutePath, type: FileLock.LockType, blocking: Bool = true, _ body: () async throws -> T) async throws -> T { + return try await FileLock.withLock(fileToLock: path.underlying, type: type, blocking: blocking, body: body) } /// Returns any known item replacement directories for a given path. These may be used by platform-specific @@ -211,9 +215,14 @@ extension FileSystem { extension FileSystem { /// SwiftPM directory under user's home directory (~/.swiftpm) + /// or under $XDG_CONFIG_HOME/swiftpm if the environmental variable is defined public var dotSwiftPM: AbsolutePath { get throws { - try self.homeDirectory.appending(".swiftpm") + if let configurationDirectory = Environment.current["XDG_CONFIG_HOME"] { + return try AbsolutePath(validating: configurationDirectory).appending("swiftpm") + } else { + return try self.homeDirectory.appending(".swiftpm") + } } } @@ -616,3 +625,73 @@ extension FileSystem { try self.removeFileTree(tempDirectory) } } + +// MARK: - Locking + +extension FileLock { + public static func prepareLock( + fileToLock: AbsolutePath, + at lockFilesDirectory: AbsolutePath? = nil + ) throws -> FileLock { + return try Self.prepareLock(fileToLock: fileToLock.underlying, at: lockFilesDirectory?.underlying) + } +} + +/// Convenience initializers for testing purposes. +extension InMemoryFileSystem { + /// Create a new file system with the given files, provided as a map from + /// file path to contents. + public convenience init(files: [String: ByteString]) { + self.init() + + for (path, contents) in files { + let path = try! AbsolutePath(validating: path) + try! createDirectory(path.parentDirectory, recursive: true) + try! writeFileContents(path, bytes: contents) + } + } + + /// Create a new file system with an empty file at each provided path. + public convenience init(emptyFiles files: String...) { + self.init(emptyFiles: files) + } + + /// Create a new file system with an empty file at each provided path. + public convenience init(emptyFiles files: [String]) { + self.init() + self.createEmptyFiles(at: .root, files: files) + } +} + +extension FileSystem { + public func createEmptyFiles(at root: AbsolutePath, files: String...) { + self.createEmptyFiles(at: root, files: files) + } + + public func createEmptyFiles(at root: AbsolutePath, files: [String]) { + do { + try createDirectory(root, recursive: true) + for path in files { + let path = try AbsolutePath(validating: String(path.dropFirst()), relativeTo: root) + try createDirectory(path.parentDirectory, recursive: true) + try writeFileContents(path, bytes: "") + } + } catch { + fatalError("Failed to create empty files: \(error)") + } + } +} + +extension FileSystem { + /// Do a deep enumeration, passing each file to block + public func enumerate(directory: AbsolutePath, block: (AbsolutePath) throws -> ()) throws { + for file in try getDirectoryContents(directory) { + let path = directory.appending(file) + if isDirectory(path) { + try enumerate(directory: path, block: block) + } else { + try block(path) + } + } + } +} diff --git a/Sources/Basics/FileSystem/InMemoryFileSystem.swift b/Sources/Basics/FileSystem/InMemoryFileSystem.swift new file mode 100644 index 00000000000..0853f607b31 --- /dev/null +++ b/Sources/Basics/FileSystem/InMemoryFileSystem.swift @@ -0,0 +1,512 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2014 - 2024 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 Swift project authors + */ + +import class Foundation.NSLock +import class Dispatch.DispatchQueue +import struct TSCBasic.AbsolutePath +import struct TSCBasic.ByteString +import class TSCBasic.FileLock +import enum TSCBasic.FileMode +import struct TSCBasic.FileSystemError + +/// Concrete FileSystem implementation which simulates an empty disk. +public final class InMemoryFileSystem: FileSystem { + /// Private internal representation of a file system node. + /// Not thread-safe. + private class Node { + /// The actual node data. + let contents: NodeContents + + /// Whether the node has executable bit enabled. + var isExecutable: Bool + + init(_ contents: NodeContents, isExecutable: Bool = false) { + self.contents = contents + self.isExecutable = isExecutable + } + + /// Creates deep copy of the object. + func copy() -> Node { + return Node(contents.copy()) + } + } + + /// Private internal representation the contents of a file system node. + /// Not thread-safe. + private enum NodeContents { + case file(ByteString) + case directory(DirectoryContents) + case symlink(String) + + /// Creates deep copy of the object. + func copy() -> NodeContents { + switch self { + case .file(let bytes): + return .file(bytes) + case .directory(let contents): + return .directory(contents.copy()) + case .symlink(let path): + return .symlink(path) + } + } + } + + /// Private internal representation the contents of a directory. + /// Not thread-safe. + private final class DirectoryContents { + var entries: [String: Node] + + init(entries: [String: Node] = [:]) { + self.entries = entries + } + + /// Creates deep copy of the object. + func copy() -> DirectoryContents { + let contents = DirectoryContents() + for (key, node) in entries { + contents.entries[key] = node.copy() + } + return contents + } + } + + /// The root node of the filesystem. + private var root: Node + + /// Protects `root` and everything underneath it. + /// FIXME: Using a single lock for this is a performance problem, but in + /// reality, the only practical use for InMemoryFileSystem is for unit + /// tests. + private let lock = NSLock() + /// A map that keeps weak references to all locked files. + private var lockFiles = Dictionary>() + /// Used to access lockFiles in a thread safe manner. + private let lockFilesLock = NSLock() + + /// Exclusive file system lock vended to clients through `withLock()`. + /// Used to ensure that DispatchQueues are released when they are no longer in use. + private struct WeakReference { + weak var reference: Value? + + init(_ value: Value?) { + self.reference = value + } + } + + public init() { + root = Node(.directory(DirectoryContents())) + } + + /// Creates deep copy of the object. + public func copy() -> InMemoryFileSystem { + return lock.withLock { + let fs = InMemoryFileSystem() + fs.root = root.copy() + return fs + } + } + + /// Private function to look up the node corresponding to a path. + /// Not thread-safe. + private func getNode(_ path: TSCBasic.AbsolutePath, followSymlink: Bool = true) throws -> Node? { + func getNodeInternal(_ path: TSCBasic.AbsolutePath) throws -> Node? { + // If this is the root node, return it. + if path.isRoot { + return root + } + + // Otherwise, get the parent node. + guard let parent = try getNodeInternal(path.parentDirectory) else { + return nil + } + + // If we didn't find a directory, this is an error. + guard case .directory(let contents) = parent.contents else { + throw FileSystemError(.notDirectory, path.parentDirectory) + } + + // Return the directory entry. + let node = contents.entries[path.basename] + + switch node?.contents { + case .directory, .file: + return node + case .symlink(let destination): + let destination = try TSCBasic.AbsolutePath(validating: destination, relativeTo: path.parentDirectory) + return followSymlink ? try getNodeInternal(destination) : node + case .none: + return nil + } + } + + // Get the node that corresponds to the path. + return try getNodeInternal(path) + } + + // MARK: FileSystem Implementation + + public func exists(_ path: TSCBasic.AbsolutePath, followSymlink: Bool) -> Bool { + return lock.withLock { + do { + switch try getNode(path, followSymlink: followSymlink)?.contents { + case .file, .directory, .symlink: return true + case .none: return false + } + } catch { + return false + } + } + } + + public func isDirectory(_ path: TSCBasic.AbsolutePath) -> Bool { + return lock.withLock { + do { + if case .directory? = try getNode(path)?.contents { + return true + } + return false + } catch { + return false + } + } + } + + public func isFile(_ path: TSCBasic.AbsolutePath) -> Bool { + return lock.withLock { + do { + if case .file? = try getNode(path)?.contents { + return true + } + return false + } catch { + return false + } + } + } + + public func isSymlink(_ path: TSCBasic.AbsolutePath) -> Bool { + return lock.withLock { + do { + if case .symlink? = try getNode(path, followSymlink: false)?.contents { + return true + } + return false + } catch { + return false + } + } + } + + public func isReadable(_ path: TSCBasic.AbsolutePath) -> Bool { + self.exists(path) + } + + public func isWritable(_ path: TSCBasic.AbsolutePath) -> Bool { + self.exists(path) + } + + public func isExecutableFile(_ path: TSCBasic.AbsolutePath) -> Bool { + (try? self.getNode(path)?.isExecutable) ?? false + } + + public func updatePermissions(_ path: AbsolutePath, isExecutable: Bool) throws { + try lock.withLock { + guard let node = try self.getNode(path.underlying, followSymlink: true) else { + throw FileSystemError(.noEntry, path) + } + node.isExecutable = isExecutable + } + } + + /// Virtualized current working directory. + private var _currentWorkingDirectory: TSCBasic.AbsolutePath = try! .init(validating: "/") + + public var currentWorkingDirectory: TSCBasic.AbsolutePath? { + return _currentWorkingDirectory + } + + public func changeCurrentWorkingDirectory(to path: TSCBasic.AbsolutePath) throws { + return try lock.withLock { + // Verify the path exists and is a directory + guard let node = try getNode(path) else { + throw FileSystemError(.noEntry, path) + } + + guard case .directory = node.contents else { + throw FileSystemError(.notDirectory, path) + } + _currentWorkingDirectory = path + } + } + + public var homeDirectory: TSCBasic.AbsolutePath { + get throws { + // FIXME: Maybe we should allow setting this when creating the fs. + return try .init(validating: "/home/user") + } + } + + public var cachesDirectory: TSCBasic.AbsolutePath? { + return try? self.homeDirectory.appending(component: "caches") + } + + public var tempDirectory: TSCBasic.AbsolutePath { + get throws { + return try .init(validating: "/tmp") + } + } + + public func getDirectoryContents(_ path: TSCBasic.AbsolutePath) throws -> [String] { + return try lock.withLock { + guard let node = try getNode(path) else { + throw FileSystemError(.noEntry, path) + } + guard case .directory(let contents) = node.contents else { + throw FileSystemError(.notDirectory, path) + } + + // FIXME: Perhaps we should change the protocol to allow lazy behavior. + return [String](contents.entries.keys) + } + } + + /// Not thread-safe. + private func _createDirectory(_ path: TSCBasic.AbsolutePath, recursive: Bool) throws { + // Ignore if client passes root. + guard !path.isRoot else { + return + } + // Get the parent directory node. + let parentPath = path.parentDirectory + guard let parent = try getNode(parentPath) else { + // If the parent doesn't exist, and we are recursive, then attempt + // to create the parent and retry. + if recursive && path != parentPath { + // Attempt to create the parent. + try _createDirectory(parentPath, recursive: true) + + // Re-attempt creation, non-recursively. + return try _createDirectory(path, recursive: false) + } else { + // Otherwise, we failed. + throw FileSystemError(.noEntry, parentPath) + } + } + + // Check that the parent is a directory. + guard case .directory(let contents) = parent.contents else { + // The parent isn't a directory, this is an error. + throw FileSystemError(.notDirectory, parentPath) + } + + // Check if the node already exists. + if let node = contents.entries[path.basename] { + // Verify it is a directory. + guard case .directory = node.contents else { + // The path itself isn't a directory, this is an error. + throw FileSystemError(.notDirectory, path) + } + + // We are done. + return + } + + // Otherwise, the node does not exist, create it. + contents.entries[path.basename] = Node(.directory(DirectoryContents())) + } + + public func createDirectory(_ path: TSCBasic.AbsolutePath, recursive: Bool) throws { + return try lock.withLock { + try _createDirectory(path, recursive: recursive) + } + } + + public func createSymbolicLink( + _ path: TSCBasic.AbsolutePath, + pointingAt destination: TSCBasic.AbsolutePath, + relative: Bool + ) throws { + return try lock.withLock { + // Create directory to destination parent. + guard let destinationParent = try getNode(path.parentDirectory) else { + throw FileSystemError(.noEntry, path.parentDirectory) + } + + // Check that the parent is a directory. + guard case .directory(let contents) = destinationParent.contents else { + throw FileSystemError(.notDirectory, path.parentDirectory) + } + + guard contents.entries[path.basename] == nil else { + throw FileSystemError(.alreadyExistsAtDestination, path) + } + + let destination = relative ? destination.relative(to: path.parentDirectory).pathString : destination.pathString + + contents.entries[path.basename] = Node(.symlink(destination)) + } + } + + public func readFileContents(_ path: TSCBasic.AbsolutePath) throws -> ByteString { + return try lock.withLock { + // Get the node. + guard let node = try getNode(path) else { + throw FileSystemError(.noEntry, path) + } + + // Check that the node is a file. + guard case .file(let contents) = node.contents else { + // The path is a directory, this is an error. + throw FileSystemError(.isDirectory, path) + } + + // Return the file contents. + return contents + } + } + + public func writeFileContents(_ path: TSCBasic.AbsolutePath, bytes: ByteString) throws { + return try lock.withLock { + // It is an error if this is the root node. + let parentPath = path.parentDirectory + guard path != parentPath else { + throw FileSystemError(.isDirectory, path) + } + + // Get the parent node. + guard let parent = try getNode(parentPath) else { + throw FileSystemError(.noEntry, parentPath) + } + + // Check that the parent is a directory. + guard case .directory(let contents) = parent.contents else { + // The parent isn't a directory, this is an error. + throw FileSystemError(.notDirectory, parentPath) + } + + // Check if the node exists. + if let node = contents.entries[path.basename] { + // Verify it is a file. + guard case .file = node.contents else { + // The path is a directory, this is an error. + throw FileSystemError(.isDirectory, path) + } + } + + // Write the file. + contents.entries[path.basename] = Node(.file(bytes)) + } + } + + public func writeFileContents(_ path: TSCBasic.AbsolutePath, bytes: ByteString, atomically: Bool) throws { + // In memory file system's writeFileContents is already atomic, so ignore the parameter here + // and just call the base implementation. + try writeFileContents(path, bytes: bytes) + } + + public func removeFileTree(_ path: TSCBasic.AbsolutePath) throws { + return lock.withLock { + // Ignore root and get the parent node's content if its a directory. + guard !path.isRoot, + let parent = try? getNode(path.parentDirectory), + case .directory(let contents) = parent.contents else { + return + } + // Set it to nil to release the contents. + contents.entries[path.basename] = nil + } + } + + public func chmod(_ mode: FileMode, path: TSCBasic.AbsolutePath, options: Set) throws { + // FIXME: We don't have these semantics in InMemoryFileSystem. + } + + /// Private implementation of core copying function. + /// Not thread-safe. + private func _copy(from sourcePath: TSCBasic.AbsolutePath, to destinationPath: TSCBasic.AbsolutePath) throws { + // Get the source node. + guard let source = try getNode(sourcePath) else { + throw FileSystemError(.noEntry, sourcePath) + } + + // Create directory to destination parent. + guard let destinationParent = try getNode(destinationPath.parentDirectory) else { + throw FileSystemError(.noEntry, destinationPath.parentDirectory) + } + + // Check that the parent is a directory. + guard case .directory(let contents) = destinationParent.contents else { + throw FileSystemError(.notDirectory, destinationPath.parentDirectory) + } + + guard contents.entries[destinationPath.basename] == nil else { + throw FileSystemError(.alreadyExistsAtDestination, destinationPath) + } + + contents.entries[destinationPath.basename] = source + } + + public func copy(from sourcePath: TSCBasic.AbsolutePath, to destinationPath: TSCBasic.AbsolutePath) throws { + return try lock.withLock { + try _copy(from: sourcePath, to: destinationPath) + } + } + + public func move(from sourcePath: TSCBasic.AbsolutePath, to destinationPath: TSCBasic.AbsolutePath) throws { + return try lock.withLock { + // Get the source parent node. + guard let sourceParent = try getNode(sourcePath.parentDirectory) else { + throw FileSystemError(.noEntry, sourcePath.parentDirectory) + } + + // Check that the parent is a directory. + guard case .directory(let contents) = sourceParent.contents else { + throw FileSystemError(.notDirectory, sourcePath.parentDirectory) + } + + try _copy(from: sourcePath, to: destinationPath) + + contents.entries[sourcePath.basename] = nil + } + } + + public func withLock( + on path: TSCBasic.AbsolutePath, + type: FileLock.LockType = .exclusive, + _ body: () throws -> T + ) throws -> T { + let resolvedPath: TSCBasic.AbsolutePath = try lock.withLock { + if case let .symlink(destination) = try getNode(path)?.contents { + return try .init(validating: destination, relativeTo: path.parentDirectory) + } else { + return path + } + } + + let fileQueue: DispatchQueue = lockFilesLock.withLock { + if let queueReference = lockFiles[resolvedPath], let queue = queueReference.reference { + return queue + } else { + let queue = DispatchQueue(label: "org.swift.swiftpm.in-memory-file-system.file-queue", attributes: .concurrent) + lockFiles[resolvedPath] = WeakReference(queue) + return queue + } + } + + return try fileQueue.sync(flags: type == .exclusive ? .barrier : .init() , execute: body) + } + + public func withLock(on path: TSCBasic.AbsolutePath, type: FileLock.LockType, blocking: Bool, _ body: () throws -> T) throws -> T { + try self.withLock(on: path, type: type, body) + } +} + +// Internal state of `InMemoryFileSystem` is protected with a lock in all of its `public` methods. +extension InMemoryFileSystem: @unchecked Sendable {} diff --git a/Sources/Basics/FileSystem/NativePathExtensions.swift b/Sources/Basics/FileSystem/NativePathExtensions.swift index a23246d9e72..de1bcb2131d 100644 --- a/Sources/Basics/FileSystem/NativePathExtensions.swift +++ b/Sources/Basics/FileSystem/NativePathExtensions.swift @@ -20,7 +20,7 @@ extension AbsolutePath { return URL(fileURLWithPath: self.pathString).withUnsafeFileSystemRepresentation { let repr = String(cString: $0!) if escaped { - return repr.replacingOccurrences(of: "\\", with: "\\\\") + return repr.replacing("\\", with: "\\\\") } return repr } diff --git a/Sources/Basics/FileSystem/TSCAdapters.swift b/Sources/Basics/FileSystem/TSCAdapters.swift index 8ef392570fb..203c359b04c 100644 --- a/Sources/Basics/FileSystem/TSCAdapters.swift +++ b/Sources/Basics/FileSystem/TSCAdapters.swift @@ -59,7 +59,6 @@ public func withTemporaryDirectory( } } -@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) public func withTemporaryDirectory( dir: AbsolutePath? = nil, prefix: String = "TemporaryDirectory", removeTreeOnDeinit: Bool = false, _ body: (AbsolutePath) async throws -> Result diff --git a/Sources/Basics/FileSystem/TemporaryFile.swift b/Sources/Basics/FileSystem/TemporaryFile.swift index a8e31126448..a9f253b6f08 100644 --- a/Sources/Basics/FileSystem/TemporaryFile.swift +++ b/Sources/Basics/FileSystem/TemporaryFile.swift @@ -34,7 +34,7 @@ public func withTemporaryDirectory( fileSystem: FileSystem = localFileSystem, dir: AbsolutePath? = nil, prefix: String = "TemporaryDirectory", - _ body: @Sendable @escaping (AbsolutePath, @escaping (AbsolutePath) -> Void) async throws -> Result + _ body: @escaping @Sendable (AbsolutePath, @escaping (AbsolutePath) -> Void) async throws -> Result ) throws -> Task { let temporaryDirectory = try createTemporaryDirectory(fileSystem: fileSystem, dir: dir, prefix: prefix) @@ -72,7 +72,7 @@ public func withTemporaryDirectory( dir: AbsolutePath? = nil, prefix: String = "TemporaryDirectory", removeTreeOnDeinit: Bool = false, - _ body: @escaping (AbsolutePath) async throws -> Result + _ body: @escaping @Sendable (AbsolutePath) async throws -> Result ) throws -> Task { try withTemporaryDirectory(fileSystem: fileSystem, dir: dir, prefix: prefix) { path, cleanup in defer { if removeTreeOnDeinit { cleanup(path) } } diff --git a/Sources/Basics/Graph/AdjacencyMatrix.swift b/Sources/Basics/Graph/AdjacencyMatrix.swift new file mode 100644 index 00000000000..9326d499a7c --- /dev/null +++ b/Sources/Basics/Graph/AdjacencyMatrix.swift @@ -0,0 +1,61 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2024 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 +// +//===----------------------------------------------------------------------===// + +/// A matrix storing bits of `true`/`false` state for a given combination of row and column indices. Used as +/// a square matrix indicating edges in graphs, where rows and columns are indices in a storage of graph's nodes. +/// +/// For example, in a graph that contains 3 nodes `matrix[row: 1, column: 2]` evaluating to `true` means an edge +/// between nodes with indices `1` and `2` exists. `matrix[row: 1, column: 2]` evaluating to `false` means that no +/// edge exists. +/// +/// See https://en.wikipedia.org/wiki/Adjacency_matrix for more details. +struct AdjacencyMatrix { + let columns: Int + let rows: Int + private var bytes: [UInt8] + + /// Allocates a new bit matrix with a given size. + /// - Parameters: + /// - rows: Number of rows in the matrix. + /// - columns: Number of columns in the matrix. + init(rows: Int, columns: Int) { + self.columns = columns + self.rows = rows + + let (quotient, remainder) = (rows * columns).quotientAndRemainder(dividingBy: 8) + self.bytes = .init(repeating: 0, count: quotient + (remainder > 0 ? 1 : 0)) + } + + var bitCount: Int { + bytes.count * 8 + } + + private func calculateOffsets(row: Int, column: Int) -> (byteOffset: Int, bitOffsetInByte: Int) { + let totalBitOffset = row * columns + column + return (byteOffset: totalBitOffset / 8, bitOffsetInByte: totalBitOffset % 8) + } + + subscript(row: Int, column: Int) -> Bool { + get { + let (byteOffset, bitOffsetInByte) = calculateOffsets(row: row, column: column) + + let result = (self.bytes[byteOffset] >> bitOffsetInByte) & 1 + return result == 1 + } + + set { + let (byteOffset, bitOffsetInByte) = calculateOffsets(row: row, column: column) + + self.bytes[byteOffset] |= 1 << bitOffsetInByte + } + } +} diff --git a/Sources/Basics/Graph/DirectedGraph.swift b/Sources/Basics/Graph/DirectedGraph.swift new file mode 100644 index 00000000000..4e58b5a2a76 --- /dev/null +++ b/Sources/Basics/Graph/DirectedGraph.swift @@ -0,0 +1,56 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2024 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 +// +//===----------------------------------------------------------------------===// + +private import DequeModule + +/// Directed graph that stores edges in [adjacency lists](https://en.wikipedia.org/wiki/Adjacency_list). +@_spi(DontAdoptOutsideOfSwiftPMExposedForBenchmarksAndTestsOnly) +public struct DirectedGraph { + public init(nodes: [Node]) { + self.nodes = nodes + self.edges = .init(repeating: [], count: nodes.count) + } + + public private(set) var nodes: [Node] + private var edges: [[Int]] + + public mutating func addEdge(source: Int, destination: Int) { + self.edges[source].append(destination) + } + + /// Checks whether a path via previously created edges between two given nodes exists. + /// - Parameters: + /// - source: `Index` of a node to start traversing edges from. + /// - destination: `Index` of a node to which a path could exist via edges from `source`. + /// - Returns: `true` if a path from `source` to `destination` exists, `false` otherwise. + @_spi(DontAdoptOutsideOfSwiftPMExposedForBenchmarksAndTestsOnly) + public func areNodesConnected(source: Int, destination: Int) -> Bool { + var todo = Deque([source]) + var done = Set() + + while !todo.isEmpty { + let nodeIndex = todo.removeFirst() + + for reachableIndex in self.edges[nodeIndex] { + if reachableIndex == destination { + return true + } else if !done.contains(reachableIndex) { + todo.append(reachableIndex) + } + } + + done.insert(nodeIndex) + } + + return false + } +} diff --git a/Sources/Basics/Graph/GraphAlgorithms.swift b/Sources/Basics/Graph/GraphAlgorithms.swift new file mode 100644 index 00000000000..8ccc6038cc0 --- /dev/null +++ b/Sources/Basics/Graph/GraphAlgorithms.swift @@ -0,0 +1,133 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2015-2024 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 struct OrderedCollections.OrderedSet + +/// Implements a pre-order depth-first search. +/// +/// The cycles are handled by skipping cycle points but it should be possible to +/// to extend this in the future to provide a callback for every cycle. +/// +/// - Parameters: +/// - nodes: The list of input nodes to sort. +/// - successors: A closure for fetching the successors of a particular node. +/// - onUnique: A callback to indicate the the given node is being processed for the first time. +/// - onDuplicate: A callback to indicate that the node was already processed at least once. +/// +/// - Complexity: O(v + e) where (v, e) are the number of vertices and edges +/// reachable from the input nodes via the relation. +public func depthFirstSearch( + _ nodes: [T], + successors: (T) throws -> [T], + onUnique: (T) throws -> Void, + onDuplicate: (T, T) -> Void +) rethrows { + var stack = OrderedSet() + var visited = Set() + + for node in nodes { + precondition(stack.isEmpty) + stack.append(node) + + while !stack.isEmpty { + let curr = stack.removeLast() + + let visitResult = visited.insert(curr) + if visitResult.inserted { + try onUnique(curr) + } else { + onDuplicate(visitResult.memberAfterInsert, curr) + continue + } + + for succ in try successors(curr) { + stack.append(succ) + } + } + } +} + +public func depthFirstSearch( + _ nodes: [T], + successors: (T) async throws -> [T], + onUnique: (T) async throws -> Void, + onDuplicate: (T, T) async -> Void +) async rethrows { + var stack = OrderedSet() + var visited = Set() + + for node in nodes { + precondition(stack.isEmpty) + stack.append(node) + + while !stack.isEmpty { + let curr = stack.removeLast() + + let visitResult = visited.insert(curr) + if visitResult.inserted { + try await onUnique(curr) + } else { + await onDuplicate(visitResult.memberAfterInsert, curr) + continue + } + + for succ in try await successors(curr) { + stack.append(succ) + } + } + } +} + +private struct TraversalNode: Hashable { + let parent: T? + let curr: T +} + +/// Implements a pre-order depth-first search that traverses the whole graph and +/// doesn't distinguish between unique and duplicate nodes. The method expects +/// the graph to be acyclic but doesn't check that. +/// +/// - Parameters: +/// - nodes: The list of input nodes to sort. +/// - successors: A closure for fetching the successors of a particular node. +/// - onNext: A callback to indicate the node currently being processed +/// including its parent (if any) and its depth. +/// +/// - Complexity: O(v + e) where (v, e) are the number of vertices and edges +/// reachable from the input nodes via the relation. +public func depthFirstSearch( + _ nodes: [T], + successors: (T) throws -> [T], + onNext: (T, _ parent: T?) throws -> Void +) rethrows { + var stack = OrderedSet>() + + for node in nodes { + precondition(stack.isEmpty) + stack.append(TraversalNode(parent: nil, curr: node)) + + while !stack.isEmpty { + let node = stack.removeLast() + + try onNext(node.curr, node.parent) + + for succ in try successors(node.curr) { + stack.append( + TraversalNode( + parent: node.curr, + curr: succ + ) + ) + } + } + } +} diff --git a/Sources/Basics/Graph/UndirectedGraph.swift b/Sources/Basics/Graph/UndirectedGraph.swift new file mode 100644 index 00000000000..1ebf7cab946 --- /dev/null +++ b/Sources/Basics/Graph/UndirectedGraph.swift @@ -0,0 +1,69 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2024 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 +// +//===----------------------------------------------------------------------===// + +private import DequeModule + +/// Undirected graph that stores edges in an [adjacency matrix](https://en.wikipedia.org/wiki/Adjacency_matrix). +@_spi(DontAdoptOutsideOfSwiftPMExposedForBenchmarksAndTestsOnly) +public struct UndirectedGraph { + public init(nodes: [Node]) { + self.nodes = nodes + self.edges = .init(rows: nodes.count, columns: nodes.count) + } + + private var nodes: [Node] + private var edges: AdjacencyMatrix + + public mutating func addEdge(source: Int, destination: Int) { + // Adjacency matrix is symmetrical for undirected graphs. + self.edges[source, destination] = true + self.edges[destination, source] = true + } + + /// Checks whether a connection via previously created edges between two given nodes exists. + /// - Parameters: + /// - source: `Index` of a node to start traversing edges from. + /// - destination: `Index` of a node to which a connection could exist via edges from `source`. + /// - Returns: `true` if a path from `source` to `destination` exists, `false` otherwise. + public func areNodesConnected(source: Int, destination: Int) -> Bool { + var todo = Deque([source]) + var done = Set() + + while !todo.isEmpty { + let nodeIndex = todo.removeFirst() + + for reachableIndex in self.edges.nodesAdjacentTo(nodeIndex) { + if reachableIndex == destination { + return true + } else if !done.contains(reachableIndex) { + todo.append(reachableIndex) + } + } + + done.insert(nodeIndex) + } + + return false + } +} + +private extension AdjacencyMatrix { + func nodesAdjacentTo(_ nodeIndex: Int) -> [Int] { + var result = [Int]() + + for i in 0..> = [] + public init(configuration: HTTPClientConfiguration = .init(), implementation: Implementation? = nil) { self.configuration = configuration self.implementation = implementation ?? URLSessionHTTPClient().execute @@ -93,6 +95,21 @@ public actor HTTPClient { return try await self.executeWithStrategies(request: request, requestNumber: 0, observabilityScope, progress) } + /// Cancel all in flight network reqeusts. + public func cancel(deadline: DispatchTime) async { + for task in activeTasks { + task.cancel() + } + + // Wait for tasks to complete or timeout + while !activeTasks.isEmpty && (deadline.distance(to: .now()).nanoseconds() ?? 0) > 0 { + await Task.yield() + } + + // Clear out the active task list regardless of whether they completed or not + activeTasks.removeAll() + } + private func executeWithStrategies( request: Request, requestNumber: Int, @@ -105,46 +122,57 @@ public actor HTTPClient { throw HTTPClientError.circuitBreakerTriggered } - let response = try await self.tokenBucket.withToken { - try await self.implementation(request) { received, expected in - if let max = request.options.maximumResponseSizeInBytes { - guard received < max else { - // It's a responsibility of the underlying client implementation to cancel the request - // when this closure throws an error - throw HTTPClientError.responseTooLarge(received) + let task = Task { + let response = try await self.tokenBucket.withToken { + try Task.checkCancellation() + + return try await self.implementation(request) { received, expected in + if let max = request.options.maximumResponseSizeInBytes { + guard received < max else { + // It's a responsibility of the underlying client implementation to cancel the request + // when this closure throws an error + throw HTTPClientError.responseTooLarge(received) + } } - } - try progress?(received, expected) + try progress?(received, expected) + } } - } - self.recordErrorIfNecessary(response: response, request: request) + self.recordErrorIfNecessary(response: response, request: request) - // handle retry strategy - if let retryDelay = self.calculateRetry( - response: response, - request: request, - requestNumber: requestNumber - ), let retryDelayInNanoseconds = retryDelay.nanoseconds() { - observabilityScope?.emit(warning: "\(request.url) failed, retrying in \(retryDelay)") - try await Task.sleep(nanoseconds: UInt64(retryDelayInNanoseconds)) - - return try await self.executeWithStrategies( + // handle retry strategy + if let retryDelay = self.calculateRetry( + response: response, request: request, - requestNumber: requestNumber + 1, - observabilityScope, - progress - ) - } - // check for valid response codes - if let validResponseCodes = request.options.validResponseCodes, - !validResponseCodes.contains(response.statusCode) - { - throw HTTPClientError.badResponseStatusCode(response.statusCode) - } else { - return response + requestNumber: requestNumber + ), let retryDelayInNanoseconds = retryDelay.nanoseconds() { + try Task.checkCancellation() + + observabilityScope?.emit(warning: "\(request.url) failed, retrying in \(retryDelay)") + try await Task.sleep(nanoseconds: UInt64(retryDelayInNanoseconds)) + + return try await self.executeWithStrategies( + request: request, + requestNumber: requestNumber + 1, + observabilityScope, + progress + ) + } + // check for valid response codes + if let validResponseCodes = request.options.validResponseCodes, + !validResponseCodes.contains(response.statusCode) + { + throw HTTPClientError.badResponseStatusCode(response.statusCode) + } else { + return response + } } + + activeTasks.insert(task) + defer { activeTasks.remove(task) } + + return try await task.value } private func calculateRetry(response: Response, request: Request, requestNumber: Int) -> SendableTimeInterval? { @@ -259,4 +287,26 @@ extension HTTPClient { Request(method: .delete, url: url, headers: headers, body: nil, options: options) ) } + + public func download( + _ url: URL, + headers: HTTPClientHeaders = .init(), + options: Request.Options = .init(), + progressHandler: ProgressHandler? = nil, + fileSystem: FileSystem, + destination: AbsolutePath, + observabilityScope: ObservabilityScope? = .none + ) async throws -> Response { + try await self.execute( + Request( + kind: .download(fileSystem: fileSystem, destination: destination), + url: url, + headers: headers, + body: nil, + options: options + ), + observabilityScope: observabilityScope, + progress: progressHandler + ) + } } diff --git a/Sources/Basics/HTTPClient/LegacyHTTPClient.swift b/Sources/Basics/HTTPClient/LegacyHTTPClient.swift index 8b53d663b4d..e47efe68203 100644 --- a/Sources/Basics/HTTPClient/LegacyHTTPClient.swift +++ b/Sources/Basics/HTTPClient/LegacyHTTPClient.swift @@ -10,6 +10,7 @@ // //===----------------------------------------------------------------------===// +import _Concurrency import Dispatch import struct Foundation.Data import struct Foundation.Date @@ -25,9 +26,9 @@ public final class LegacyHTTPClient: Cancellable { public typealias Configuration = LegacyHTTPClientConfiguration public typealias Request = LegacyHTTPClientRequest public typealias Response = HTTPClientResponse - public typealias Handler = (Request, ProgressHandler?, @escaping (Result) -> Void) -> Void - public typealias ProgressHandler = (_ bytesReceived: Int64, _ totalBytes: Int64?) throws -> Void - public typealias CompletionHandler = (Result) -> Void + public typealias Handler = (Request, ProgressHandler?, @escaping @Sendable (Result) -> Void) -> Void + public typealias ProgressHandler = @Sendable (_ bytesReceived: Int64, _ totalBytes: Int64?) throws -> Void + public typealias CompletionHandler = @Sendable (Result) -> Void public var configuration: LegacyHTTPClientConfiguration private let underlying: Handler @@ -48,7 +49,7 @@ public final class LegacyHTTPClient: Cancellable { private var outstandingRequests = ThreadSafeKeyValueStore() // static to share across instances of the http client - private static var hostsErrorsLock = NSLock() + private static let hostsErrorsLock = NSLock() private static var hostsErrors = [String: [Date]]() public init(configuration: LegacyHTTPClientConfiguration = .init(), handler: Handler? = nil) { @@ -121,7 +122,7 @@ public final class LegacyHTTPClient: Cancellable { requestNumber: 0, observabilityScope: observabilityScope, progress: progress.map { handler in - { received, expected in + { @Sendable received, expected in // call back on the requested queue callbackQueue.async { do { @@ -307,12 +308,23 @@ public final class LegacyHTTPClient: Cancellable { } extension LegacyHTTPClient { + public func head( + _ url: URL, + headers: HTTPClientHeaders = .init(), + options: Request.Options = .init(), + observabilityScope: ObservabilityScope? = .none + ) async throws -> Response { + try await withCheckedThrowingContinuation { continuation in + self.head(url, headers: headers, options: options, completion: { continuation.resume(with: $0) }) + } + } + @available(*, noasync, message: "Use the async alternative") public func head( _ url: URL, headers: HTTPClientHeaders = .init(), options: Request.Options = .init(), observabilityScope: ObservabilityScope? = .none, - completion: @escaping (Result) -> Void + completion: @Sendable @escaping (Result) -> Void ) { self.execute( Request(method: .head, url: url, headers: headers, body: nil, options: options), @@ -321,12 +333,23 @@ extension LegacyHTTPClient { ) } + public func get( + _ url: URL, + headers: HTTPClientHeaders = .init(), + options: Request.Options = .init(), + observabilityScope: ObservabilityScope? = .none + ) async throws -> Response { + try await withCheckedThrowingContinuation { continuation in + self.get(url, headers: headers, options: options, completion: { continuation.resume(with: $0) }) + } + } + @available(*, noasync, message: "Use the async alternative") public func get( _ url: URL, headers: HTTPClientHeaders = .init(), options: Request.Options = .init(), observabilityScope: ObservabilityScope? = .none, - completion: @escaping (Result) -> Void + completion: @Sendable @escaping (Result) -> Void ) { self.execute( Request(method: .get, url: url, headers: headers, body: nil, options: options), @@ -341,7 +364,7 @@ extension LegacyHTTPClient { headers: HTTPClientHeaders = .init(), options: Request.Options = .init(), observabilityScope: ObservabilityScope? = .none, - completion: @escaping (Result) -> Void + completion: @Sendable @escaping (Result) -> Void ) { self.execute( Request(method: .put, url: url, headers: headers, body: body, options: options), @@ -356,7 +379,7 @@ extension LegacyHTTPClient { headers: HTTPClientHeaders = .init(), options: Request.Options = .init(), observabilityScope: ObservabilityScope? = .none, - completion: @escaping (Result) -> Void + completion: @Sendable @escaping (Result) -> Void ) { self.execute( Request(method: .post, url: url, headers: headers, body: body, options: options), @@ -370,7 +393,7 @@ extension LegacyHTTPClient { headers: HTTPClientHeaders = .init(), options: Request.Options = .init(), observabilityScope: ObservabilityScope? = .none, - completion: @escaping (Result) -> Void + completion: @Sendable @escaping (Result) -> Void ) { self.execute( Request(method: .delete, url: url, headers: headers, body: nil, options: options), @@ -383,7 +406,7 @@ extension LegacyHTTPClient { // MARK: - LegacyHTTPClientConfiguration public struct LegacyHTTPClientConfiguration { - public typealias AuthorizationProvider = (URL) -> String? + public typealias AuthorizationProvider = @Sendable (URL) -> String? public var requestHeaders: HTTPClientHeaders? public var requestTimeout: DispatchTimeInterval? diff --git a/Sources/Basics/HTTPClient/LegacyHTTPClientRequest.swift b/Sources/Basics/HTTPClient/LegacyHTTPClientRequest.swift index 58ce1880c45..9948a2c1c83 100644 --- a/Sources/Basics/HTTPClient/LegacyHTTPClientRequest.swift +++ b/Sources/Basics/HTTPClient/LegacyHTTPClientRequest.swift @@ -12,7 +12,7 @@ import Foundation -public struct LegacyHTTPClientRequest { +public struct LegacyHTTPClientRequest: Sendable { public let kind: Kind public let url: URL public var headers: HTTPClientHeaders @@ -73,12 +73,12 @@ public struct LegacyHTTPClientRequest { public typealias FileMoveCompletion = @Sendable (Error?) -> Void - public enum Kind { + public enum Kind: Sendable { case generic(HTTPMethod) case download(fileSystem: FileSystem, destination: AbsolutePath) } - public struct Options { + public struct Options: Sendable { public init( addUserAgent: Bool = true, validResponseCodes: [Int]? = nil, diff --git a/Sources/Basics/HTTPClient/URLSessionHTTPClient.swift b/Sources/Basics/HTTPClient/URLSessionHTTPClient.swift index d6302003d2a..a12c352f725 100644 --- a/Sources/Basics/HTTPClient/URLSessionHTTPClient.swift +++ b/Sources/Basics/HTTPClient/URLSessionHTTPClient.swift @@ -14,18 +14,41 @@ import _Concurrency import Foundation import struct TSCUtility.Versioning #if canImport(FoundationNetworking) -// FIXME: this brings OpenSSL dependency on Linux -// need to decide how to best deal with that +// FIXME: this brings OpenSSL dependency on Linux and needs to be replaced with `swift-server/async-http-client` package import FoundationNetworking #endif -final class URLSessionHTTPClient { +final class URLSessionHTTPClient: Sendable { + private let dataSession: URLSession + private let downloadSession: URLSession private let dataTaskManager: DataTaskManager private let downloadTaskManager: DownloadTaskManager init(configuration: URLSessionConfiguration = .default) { - self.dataTaskManager = DataTaskManager(configuration: configuration) - self.downloadTaskManager = DownloadTaskManager(configuration: configuration) + let dataDelegateQueue = OperationQueue() + dataDelegateQueue.name = "org.swift.swiftpm.urlsession-http-client-data-delegate" + dataDelegateQueue.maxConcurrentOperationCount = 1 + self.dataTaskManager = DataTaskManager() + self.dataSession = URLSession( + configuration: configuration, + delegate: self.dataTaskManager, + delegateQueue: dataDelegateQueue + ) + + let downloadDelegateQueue = OperationQueue() + downloadDelegateQueue.name = "org.swift.swiftpm.urlsession-http-client-download-delegate" + downloadDelegateQueue.maxConcurrentOperationCount = 1 + self.downloadTaskManager = DownloadTaskManager() + self.downloadSession = URLSession( + configuration: configuration, + delegate: self.downloadTaskManager, + delegateQueue: downloadDelegateQueue + ) + } + + deinit { + dataSession.finishTasksAndInvalidate() + downloadSession.finishTasksAndInvalidate() } @Sendable @@ -38,27 +61,35 @@ final class URLSessionHTTPClient { let task: URLSessionTask switch request.kind { case .generic: - task = self.dataTaskManager.makeTask( + let dataTask = self.dataSession.dataTask(with: urlRequest) + self.dataTaskManager.register( + task: dataTask, urlRequest: urlRequest, authorizationProvider: request.options.authorizationProvider, progress: progress, - completion: continuation.resume(with:) + completion: { continuation.resume(with: $0) } ) + task = dataTask case .download(_, let destination): - task = self.downloadTaskManager.makeTask( + let downloadTask = self.downloadSession.downloadTask(with: urlRequest) + self.downloadTaskManager.register( + task: downloadTask, urlRequest: urlRequest, - // FIXME: always using a synchronous filesystem, because `URLSessionDownloadDelegate` + authorizationProvider: request.options.authorizationProvider, + // FIXME: always using synchronous filesystem, because `URLSessionDownloadDelegate` // needs temporary files to moved out of temporary locations synchronously in delegate callbacks. fileSystem: localFileSystem, destination: destination, progress: progress, - completion: continuation.resume(with:) + completion: { continuation.resume(with: $0) } ) + task = downloadTask } task.resume() } } + @Sendable public func execute( _ request: LegacyHTTPClient.Request, progress: LegacyHTTPClient.ProgressHandler?, @@ -68,52 +99,49 @@ final class URLSessionHTTPClient { let task: URLSessionTask switch request.kind { case .generic: - task = self.dataTaskManager.makeTask( + let dataTask = self.dataSession.dataTask(with: urlRequest) + self.dataTaskManager.register( + task: dataTask, urlRequest: urlRequest, authorizationProvider: request.options.authorizationProvider, progress: progress, completion: completion ) + task = dataTask case .download(let fileSystem, let destination): - task = self.downloadTaskManager.makeTask( + let downloadTask = self.downloadSession.downloadTask(with: urlRequest) + self.downloadTaskManager.register( + task: downloadTask, urlRequest: urlRequest, + authorizationProvider: request.options.authorizationProvider, fileSystem: fileSystem, destination: destination, progress: progress, completion: completion ) + task = downloadTask } task.resume() } } -private class DataTaskManager: NSObject, URLSessionDataDelegate { - private var tasks = ThreadSafeKeyValueStore() - private let delegateQueue: OperationQueue - private var session: URLSession! - - public init(configuration: URLSessionConfiguration) { - self.delegateQueue = OperationQueue() - self.delegateQueue.name = "org.swift.swiftpm.urlsession-http-client-data-delegate" - self.delegateQueue.maxConcurrentOperationCount = 1 - super.init() - self.session = URLSession(configuration: configuration, delegate: self, delegateQueue: self.delegateQueue) - } +private final class DataTaskManager: NSObject, URLSessionDataDelegate { + private let tasks = ThreadSafeKeyValueStore() - func makeTask( + func register( + task: URLSessionDataTask, urlRequest: URLRequest, authorizationProvider: LegacyHTTPClientConfiguration.AuthorizationProvider?, progress: LegacyHTTPClient.ProgressHandler?, completion: @escaping LegacyHTTPClient.CompletionHandler - ) -> URLSessionDataTask { - let task = self.session.dataTask(with: urlRequest) + ) { self.tasks[task.taskIdentifier] = DataTask( task: task, progressHandler: progress, + dataTaskManager: self, completionHandler: completion, authorizationProvider: authorizationProvider ) - return task } public func urlSession( @@ -122,11 +150,13 @@ private class DataTaskManager: NSObject, URLSessionDataDelegate { didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void ) { - guard let task = self.tasks[dataTask.taskIdentifier] else { + guard var task = self.tasks[dataTask.taskIdentifier] else { return completionHandler(.cancel) } task.response = response as? HTTPURLResponse task.expectedContentLength = response.expectedContentLength + self.tasks[dataTask.taskIdentifier] = task + do { try task.progressHandler?(0, response.expectedContentLength) completionHandler(.allow) @@ -136,7 +166,7 @@ private class DataTaskManager: NSObject, URLSessionDataDelegate { } public func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { - guard let task = self.tasks[dataTask.taskIdentifier] else { + guard var task = self.tasks[dataTask.taskIdentifier] else { return } if task.buffer != nil { @@ -144,6 +174,7 @@ private class DataTaskManager: NSObject, URLSessionDataDelegate { } else { task.buffer = data } + self.tasks[dataTask.taskIdentifier] = task do { // safe since created in the line above @@ -189,9 +220,14 @@ private class DataTaskManager: NSObject, URLSessionDataDelegate { completionHandler(request) } - class DataTask { + struct DataTask: Sendable { let task: URLSessionDataTask let completionHandler: LegacyHTTPClient.CompletionHandler + /// A strong reference to keep the `DataTaskManager` alive so it can handle the callbacks from the + /// `URLSession`. + /// + /// See comment on `WeakDataTaskManager`. + let dataTaskManager: DataTaskManager let progressHandler: LegacyHTTPClient.ProgressHandler? let authorizationProvider: LegacyHTTPClientConfiguration.AuthorizationProvider? @@ -202,46 +238,40 @@ private class DataTaskManager: NSObject, URLSessionDataDelegate { init( task: URLSessionDataTask, progressHandler: LegacyHTTPClient.ProgressHandler?, + dataTaskManager: DataTaskManager, completionHandler: @escaping LegacyHTTPClient.CompletionHandler, authorizationProvider: LegacyHTTPClientConfiguration.AuthorizationProvider? ) { self.task = task self.progressHandler = progressHandler + self.dataTaskManager = dataTaskManager self.completionHandler = completionHandler self.authorizationProvider = authorizationProvider } } } -private class DownloadTaskManager: NSObject, URLSessionDownloadDelegate { - private var tasks = ThreadSafeKeyValueStore() - private let delegateQueue: OperationQueue - private var session: URLSession! - - init(configuration: URLSessionConfiguration) { - self.delegateQueue = OperationQueue() - self.delegateQueue.name = "org.swift.swiftpm.urlsession-http-client-download-delegate" - self.delegateQueue.maxConcurrentOperationCount = 1 - super.init() - self.session = URLSession(configuration: configuration, delegate: self, delegateQueue: self.delegateQueue) - } +private final class DownloadTaskManager: NSObject, URLSessionDownloadDelegate { + private let tasks = ThreadSafeKeyValueStore() - func makeTask( + func register( + task: URLSessionDownloadTask, urlRequest: URLRequest, + authorizationProvider: LegacyHTTPClientConfiguration.AuthorizationProvider?, fileSystem: FileSystem, destination: AbsolutePath, progress: LegacyHTTPClient.ProgressHandler?, completion: @escaping LegacyHTTPClient.CompletionHandler - ) -> URLSessionDownloadTask { - let task = self.session.downloadTask(with: urlRequest) + ) { self.tasks[task.taskIdentifier] = DownloadTask( task: task, fileSystem: fileSystem, destination: destination, + downloadTaskManager: self, progressHandler: progress, - completionHandler: completion + completionHandler: completion, + authorizationProvider: authorizationProvider ) - return task } func urlSession( @@ -270,7 +300,7 @@ private class DownloadTaskManager: NSObject, URLSessionDownloadDelegate { downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL ) { - guard let task = self.tasks[downloadTask.taskIdentifier] else { + guard var task = self.tasks[downloadTask.taskIdentifier] else { return } @@ -283,6 +313,7 @@ private class DownloadTaskManager: NSObject, URLSessionDownloadDelegate { try task.fileSystem.move(from: path, to: task.destination) } catch { task.moveFileError = error + self.tasks[downloadTask.taskIdentifier] = task } } @@ -310,12 +341,35 @@ private class DownloadTaskManager: NSObject, URLSessionDownloadDelegate { } } - class DownloadTask { + public func urlSession( + _ session: URLSession, + task: URLSessionTask, + willPerformHTTPRedirection response: HTTPURLResponse, + newRequest request: URLRequest, + completionHandler: @escaping (URLRequest?) -> Void + ) { + guard let task = self.tasks[task.taskIdentifier] else { + return + } + + // Add new authorization header for a redirect if there is one, otherwise remove + var redirectRequest = request + if let redirectURL = request.url, let authorization = task.authorizationProvider?(redirectURL) { + redirectRequest.setValue(authorization, forHTTPHeaderField: "Authorization") + } else { + redirectRequest.setValue(nil, forHTTPHeaderField: "Authorization") + } + + completionHandler(redirectRequest) + } + + struct DownloadTask: Sendable { let task: URLSessionDownloadTask let fileSystem: FileSystem let destination: AbsolutePath - let completionHandler: LegacyHTTPClient.CompletionHandler let progressHandler: LegacyHTTPClient.ProgressHandler? + let completionHandler: LegacyHTTPClient.CompletionHandler + let authorizationProvider: LegacyHTTPClientConfiguration.AuthorizationProvider? var moveFileError: Error? @@ -323,14 +377,17 @@ private class DownloadTaskManager: NSObject, URLSessionDownloadDelegate { task: URLSessionDownloadTask, fileSystem: FileSystem, destination: AbsolutePath, + downloadTaskManager: DownloadTaskManager, progressHandler: LegacyHTTPClient.ProgressHandler?, - completionHandler: @escaping LegacyHTTPClient.CompletionHandler + completionHandler: @escaping LegacyHTTPClient.CompletionHandler, + authorizationProvider: LegacyHTTPClientConfiguration.AuthorizationProvider? ) { self.task = task self.fileSystem = fileSystem self.destination = destination self.progressHandler = progressHandler self.completionHandler = completionHandler + self.authorizationProvider = authorizationProvider } } } diff --git a/Sources/Basics/ImportScanning.swift b/Sources/Basics/ImportScanning.swift index b517655d629..bd4b4576217 100644 --- a/Sources/Basics/ImportScanning.swift +++ b/Sources/Basics/ImportScanning.swift @@ -13,7 +13,6 @@ import Dispatch import class Foundation.JSONDecoder -import class TSCBasic.Process private let defaultImports = ["Swift", "SwiftOnoneSupport", "_Concurrency", "_StringProcessing", "_SwiftConcurrencyShims"] @@ -22,17 +21,17 @@ private struct Imports: Decodable { let imports: [String] } -public protocol ImportScanner { +package protocol ImportScanner { func scanImports(_ filePathToScan: AbsolutePath) async throws -> [String] } public struct SwiftcImportScanner: ImportScanner { - private let swiftCompilerEnvironment: EnvironmentVariables + private let swiftCompilerEnvironment: Environment private let swiftCompilerFlags: [String] private let swiftCompilerPath: AbsolutePath - public init( - swiftCompilerEnvironment: EnvironmentVariables, + package init( + swiftCompilerEnvironment: Environment, swiftCompilerFlags: [String], swiftCompilerPath: AbsolutePath ) { @@ -46,7 +45,7 @@ public struct SwiftcImportScanner: ImportScanner { filePathToScan.pathString, "-scan-dependencies", "-Xfrontend", "-import-prescan"] + self.swiftCompilerFlags - let result = try await TSCBasic.Process.popen(arguments: cmd, environment: self.swiftCompilerEnvironment) + let result = try await AsyncProcess.popen(arguments: cmd, environment: self.swiftCompilerEnvironment) let stdout = try result.utf8Output() return try JSONDecoder.makeWithDefaults().decode(Imports.self, from: stdout).imports diff --git a/Sources/Basics/JSON+Extensions.swift b/Sources/Basics/JSON+Extensions.swift index de4c67d79ce..94c35fe7442 100644 --- a/Sources/Basics/JSON+Extensions.swift +++ b/Sources/Basics/JSON+Extensions.swift @@ -15,64 +15,8 @@ import class Foundation.DateFormatter import class Foundation.JSONDecoder import class Foundation.JSONEncoder -#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS) -extension DateFormatter { - public static let iso8601: DateFormatter = { - let dateFormatter = DateFormatter() - dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ" - return dateFormatter - }() -} - -extension JSONEncoder.DateEncodingStrategy { - public static let customISO8601 = custom { - var container = $1.singleValueContainer() - try container.encode(DateFormatter.iso8601.string(from: $0)) - } -} - -extension JSONDecoder.DateDecodingStrategy { - public static let customISO8601 = custom { - let container = try $0.singleValueContainer() - let string = try container.decode(String.self) - if let date = DateFormatter.iso8601.date(from: string) { - return date - } - throw DecodingError.dataCorruptedError(in: container, debugDescription: "Invalid date: \(string)") - } -} -#endif - -extension JSONEncoder.DateEncodingStrategy { - public static var safeISO8601: JSONEncoder.DateEncodingStrategy { - #if os(macOS) || os(iOS) || os(tvOS) || os(watchOS) - if #available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) { - return .iso8601 - } else { - return .customISO8601 - } - #else - return .iso8601 - #endif - } -} - -extension JSONDecoder.DateDecodingStrategy { - public static var safeISO8601: JSONDecoder.DateDecodingStrategy { - #if os(macOS) || os(iOS) || os(tvOS) || os(watchOS) - if #available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) { - return .iso8601 - } else { - return .customISO8601 - } - #else - return .iso8601 - #endif - } -} - extension JSONDecoder { - public static func makeWithDefaults(dateDecodingStrategy: DateDecodingStrategy = .safeISO8601) -> JSONDecoder { + public static func makeWithDefaults(dateDecodingStrategy: DateDecodingStrategy = .iso8601) -> JSONDecoder { let decoder = JSONDecoder() decoder.dateDecodingStrategy = dateDecodingStrategy return decoder @@ -82,7 +26,7 @@ extension JSONDecoder { extension JSONEncoder { public static func makeWithDefaults( prettified: Bool = true, - dateEncodingStrategy: DateEncodingStrategy = .safeISO8601 + dateEncodingStrategy: DateEncodingStrategy = .iso8601 ) -> JSONEncoder { Self.makeWithDefaults( sortKeys: prettified, @@ -96,31 +40,19 @@ extension JSONEncoder { sortKeys: Bool, prettyPrint: Bool, escapeSlashes: Bool, - dateEncodingStrategy: DateEncodingStrategy = .safeISO8601 + dateEncodingStrategy: DateEncodingStrategy = .iso8601 ) -> JSONEncoder { let encoder = JSONEncoder() var outputFormatting: JSONEncoder.OutputFormatting = [] if sortKeys { - #if os(macOS) || os(iOS) || os(tvOS) || os(watchOS) - if #available(macOS 10.15, iOS 11.0, watchOS 4.0, tvOS 11.0, *) { - outputFormatting.insert(.sortedKeys) - } - #else outputFormatting.insert(.sortedKeys) - #endif } if prettyPrint { outputFormatting.insert(.prettyPrinted) } if !escapeSlashes { - #if os(macOS) || os(iOS) || os(tvOS) || os(watchOS) - if #available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) { - outputFormatting.insert(.withoutEscapingSlashes) - } - #elseif compiler(>=5.3) outputFormatting.insert(.withoutEscapingSlashes) - #endif } encoder.outputFormatting = outputFormatting diff --git a/Sources/Basics/Netrc.swift b/Sources/Basics/Netrc.swift index 4bbd211cd78..cbe88206895 100644 --- a/Sources/Basics/Netrc.swift +++ b/Sources/Basics/Netrc.swift @@ -13,7 +13,7 @@ import Foundation /// Representation of Netrc configuration -public struct Netrc { +public struct Netrc: Sendable { /// Representation of `machine` connection settings & `default` connection settings. /// If `default` connection settings present, they will be last element. public let machines: [Machine] @@ -37,7 +37,7 @@ public struct Netrc { } /// Representation of connection settings - public struct Machine: Equatable { + public struct Machine: Equatable, Sendable { public let name: String public let login: String public let password: String @@ -132,8 +132,11 @@ public struct NetrcParser { let matches = regex.matches(in: text, range: range) var trimmedCommentsText = text matches.forEach { - trimmedCommentsText = trimmedCommentsText - .replacingOccurrences(of: nsString.substring(with: $0.range), with: "") + let matchedString = nsString.substring(with: $0.range) + if !matchedString.starts(with: "\"") { + trimmedCommentsText = trimmedCommentsText + .replacing(matchedString, with: "") + } } return trimmedCommentsText } @@ -147,17 +150,22 @@ public enum NetrcError: Error, Equatable { } private enum RegexUtil { - @frozen fileprivate enum Token: String, CaseIterable { case machine, login, password, account, macdef, `default` func capture(prefix: String = "", in match: NSTextCheckingResult, string: String) -> String? { - guard let range = Range(match.range(withName: prefix + rawValue), in: string) else { return nil } - return String(string[range]) + if let quotedRange = Range(match.range(withName: prefix + rawValue + quotedIdentifier), in: string) { + return String(string[quotedRange]) + } else if let range = Range(match.range(withName: prefix + rawValue), in: string) { + return String(string[range]) + } else { + return nil + } } } - static let comments: String = "\\#[\\s\\S]*?.*$" + private static let quotedIdentifier = "quoted" + static let comments: String = "(\"[^\"]*\"|\\s#.*$)" static let `default`: String = #"(?:\s*(?default))"# static let accountOptional: String = #"(?:\s*account\s+\S++)?"# static let loginPassword: String = @@ -172,6 +180,6 @@ private enum RegexUtil { } static func namedTrailingCapture(_ string: String, prefix: String = "") -> String { - #"\s*\#(string)\s+(?<\#(prefix + string)>\S++)"# + #"\s*\#(string)\s+(?:"(?<\#(prefix + string + quotedIdentifier)>[^"]*)"|(?<\#(prefix + string)>\S+))"# } } diff --git a/Sources/Basics/OSSignpost.swift b/Sources/Basics/OSSignpost.swift index 34eb9d49c04..be8e4a83609 100644 --- a/Sources/Basics/OSSignpost.swift +++ b/Sources/Basics/OSSignpost.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift open source project // -// Copyright (c) 2023 Apple Inc. and the Swift project authors +// Copyright (c) 2023-2024 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 @@ -12,9 +12,7 @@ #if canImport(os) import os -#endif -#if canImport(os) extension os.OSLog { @usableFromInline static let swiftpm = os.OSLog(subsystem: "org.swift.swiftpm", category: "default") @@ -22,7 +20,7 @@ extension os.OSLog { #endif /// Emits a signpost. -@inlinable public func os_signpost( +@inlinable package func os_signpost( _ type: SignpostType, name: StaticString, signpostID: SignpostID = .exclusive @@ -39,8 +37,8 @@ extension os.OSLog { #endif } - -public enum SignpostType { +@usableFromInline +package enum SignpostType { case begin case end case event @@ -61,7 +59,8 @@ public enum SignpostType { #endif } -public enum SignpostID { +@usableFromInline +package enum SignpostID { case exclusive #if canImport(os) @@ -77,7 +76,7 @@ public enum SignpostID { } -public enum SignpostName { +package enum SignpostName { public static let updatingDependencies: StaticString = "updating" public static let resolvingDependencies: StaticString = "resolving" public static let pubgrub: StaticString = "pubgrub" diff --git a/Sources/Basics/Observability.swift b/Sources/Basics/Observability.swift index 4d6e01f959b..404d42a85c4 100644 --- a/Sources/Basics/Observability.swift +++ b/Sources/Basics/Observability.swift @@ -16,6 +16,7 @@ import Foundation import struct TSCBasic.Diagnostic import protocol TSCBasic.DiagnosticData import protocol TSCBasic.DiagnosticLocation +import class TSCBasic.TerminalController import class TSCBasic.UnknownLocation import protocol TSCUtility.DiagnosticDataConvertible import enum TSCUtility.Diagnostics @@ -46,8 +47,7 @@ public class ObservabilitySystem { private struct SingleDiagnosticsHandler: ObservabilityHandlerProvider, DiagnosticsHandler { var diagnosticsHandler: DiagnosticsHandler { self } - let underlying: @Sendable (ObservabilityScope, Diagnostic) - -> Void + let underlying: @Sendable (ObservabilityScope, Diagnostic) -> Void init(_ underlying: @escaping @Sendable (ObservabilityScope, Diagnostic) -> Void) { self.underlying = underlying @@ -57,6 +57,10 @@ public class ObservabilitySystem { self.underlying(scope, diagnostic) } } + + public static var NOOP: ObservabilityScope { + ObservabilitySystem { _, _ in }.topScope + } } public protocol ObservabilityHandlerProvider { @@ -158,7 +162,7 @@ public protocol DiagnosticsHandler: Sendable { func handleDiagnostic(scope: ObservabilityScope, diagnostic: Diagnostic) } -// helper protocol to share default behavior +/// Helper protocol to share default behavior. public protocol DiagnosticsEmitterProtocol { func emit(_ diagnostic: Diagnostic) } @@ -247,6 +251,18 @@ extension DiagnosticsEmitterProtocol { } } + public func trap(_ closure: () async throws -> T) async -> T? { + do { + return try await closure() + } catch Diagnostics.fatalError { + // FIXME: (diagnostics) deprecate this with Diagnostics.fatalError + return nil + } catch { + self.emit(error) + return nil + } + } + /// trap a throwing closure, emitting diagnostics on error and returning boolean representing success @discardableResult public func trap(_ closure: () throws -> Void) -> Bool { @@ -262,6 +278,20 @@ extension DiagnosticsEmitterProtocol { } } + @discardableResult + public func trap(_ closure: () async throws -> Void) async -> Bool { + do { + try await closure() + return true + } catch Diagnostics.fatalError { + // FIXME: (diagnostics) deprecate this with Diagnostics.fatalError + return false + } catch { + self.emit(error) + return false + } + } + /// If `underlyingError` is not `nil`, its human-readable description is interpolated with `message`, /// otherwise `message` itself is returned. private func makeMessage(from message: String, underlyingError: Error?) -> String { @@ -364,7 +394,7 @@ public struct Diagnostic: Sendable, CustomStringConvertible { case info case debug - internal var naturalIntegralValue: Int { + var naturalIntegralValue: Int { switch self { case .debug: return 0 @@ -380,6 +410,47 @@ public struct Diagnostic: Sendable, CustomStringConvertible { public static func < (lhs: Self, rhs: Self) -> Bool { lhs.naturalIntegralValue < rhs.naturalIntegralValue } + + /// A string that represents the log label associated with the severity level. + /// This property provides a descriptive prefix for log messages, indicating the type of message based on its + /// severity. + public var logLabel: String { + switch self { + case .debug: + return "debug: " + case .info: + return "info: " + case .warning: + return "warning: " + case .error: + return "error: " + } + } + + public var color: TerminalController.Color { + switch self { + case .debug: + return .white + case .info: + return .white + case .error: + return .red + case .warning: + return .yellow + } + } + + public var isBold: Bool { + return true + } + + public var isVerbose: Bool { + self <= .info + } + + public var isQuiet: Bool { + self >= .error + } } } @@ -388,7 +459,8 @@ public struct Diagnostic: Sendable, CustomStringConvertible { /// Provides type-safe access to the ObservabilityMetadata's values. /// This API should ONLY be used inside of accessor implementations. /// -/// End users should use "accessors" the key's author MUST define rather than using this subscript, following this pattern: +/// End users should use "accessors" the key's author MUST define rather than using this subscript, following this +/// pattern: /// /// extension ObservabilityMetadata { /// var testID: String? { @@ -509,7 +581,8 @@ public struct ObservabilityMetadata: Sendable, CustomDebugStringConvertible { } } - /// A type-erased `ObservabilityMetadataKey` used when iterating through the `ObservabilityMetadata` using its `forEach` method. + /// A type-erased `ObservabilityMetadataKey` used when iterating through the `ObservabilityMetadata` using its + /// `forEach` method. public struct AnyKey: Sendable { /// The key's type represented erased to an `Any.Type`. public let keyType: Any.Type diff --git a/Sources/Basics/Process.swift b/Sources/Basics/Process.swift new file mode 100644 index 00000000000..91eb926655c --- /dev/null +++ b/Sources/Basics/Process.swift @@ -0,0 +1,58 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2014 - 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 Swift project authors + */ + +public import Foundation + +public enum OperatingSystem: Hashable, Sendable { + case macOS + case windows + case linux + case android + case freebsd + case openbsd + case unknown +} + + +extension ProcessInfo { + public static var hostOperatingSystem: OperatingSystem { + #if os(macOS) + .macOS + #elseif os(Linux) + .linux + #elseif os(Windows) + .windows + #elseif os(FreeBSD) + .freebsd + #elseif os(OpenBSD) + .openbsd + #else + .unknown + #endif + } + + #if os(Windows) + public static let EOL = "\r\n" + #else + public static let EOL = "\n" + #endif + + #if os(Windows) + public static let exeSuffix = ".exe" + #else + public static let exeSuffix = "" + #endif + + #if os(Windows) + public static let batSuffix = ".bat" + #else + public static let batSuffix = "" + #endif +} diff --git a/Sources/Basics/ProgressAnimation/NinjaProgressAnimation.swift b/Sources/Basics/ProgressAnimation/NinjaProgressAnimation.swift new file mode 100644 index 00000000000..5a42adec7c0 --- /dev/null +++ b/Sources/Basics/ProgressAnimation/NinjaProgressAnimation.swift @@ -0,0 +1,102 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2014-2024 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 class TSCBasic.TerminalController +import protocol TSCBasic.WritableByteStream + +extension ProgressAnimation { + /// A ninja-like progress animation that adapts to the provided output stream. + @_spi(SwiftPMInternal) + public static func ninja( + stream: WritableByteStream, + verbose: Bool + ) -> any ProgressAnimationProtocol { + Self.dynamic( + stream: stream, + verbose: verbose, + ttyTerminalAnimationFactory: { RedrawingNinjaProgressAnimation(terminal: $0) }, + dumbTerminalAnimationFactory: { SingleLinePercentProgressAnimation(stream: stream, header: nil) }, + defaultAnimationFactory: { MultiLineNinjaProgressAnimation(stream: stream) } + ) + } +} + +/// A redrawing ninja-like progress animation. +final class RedrawingNinjaProgressAnimation: ProgressAnimationProtocol { + private let terminal: TerminalController + private var hasDisplayedProgress = false + + init(terminal: TerminalController) { + self.terminal = terminal + } + + func update(step: Int, total: Int, text: String) { + assert(step <= total) + + terminal.clearLine() + + let progressText = "[\(step)/\(total)] \(text)" + let width = terminal.width + if progressText.utf8.count > width { + let suffix = "…" + terminal.write(String(progressText.prefix(width - suffix.utf8.count))) + terminal.write(suffix) + } else { + terminal.write(progressText) + } + + hasDisplayedProgress = true + } + + func complete(success: Bool) { + if hasDisplayedProgress { + terminal.endLine() + } + } + + func clear() { + terminal.clearLine() + } +} + +/// A multi-line ninja-like progress animation. +final class MultiLineNinjaProgressAnimation: ProgressAnimationProtocol { + private struct Info: Equatable { + let step: Int + let total: Int + let text: String + } + + private let stream: WritableByteStream + private var lastDisplayedText: String? = nil + + init(stream: WritableByteStream) { + self.stream = stream + } + + func update(step: Int, total: Int, text: String) { + assert(step <= total) + + guard text != lastDisplayedText else { return } + + stream.send("[\(step)/\(total)] ").send(text) + stream.send("\n") + stream.flush() + lastDisplayedText = text + } + + func complete(success: Bool) { + } + + func clear() { + } +} diff --git a/Sources/Basics/ProgressAnimation/PercentProgressAnimation.swift b/Sources/Basics/ProgressAnimation/PercentProgressAnimation.swift new file mode 100644 index 00000000000..fc6d4587e26 --- /dev/null +++ b/Sources/Basics/ProgressAnimation/PercentProgressAnimation.swift @@ -0,0 +1,157 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2014-2024 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 class TSCBasic.TerminalController +import protocol TSCBasic.WritableByteStream + +extension ProgressAnimation { + /// A percent-based progress animation that adapts to the provided output stream. + @_spi(SwiftPMInternal) + public static func percent( + stream: WritableByteStream, + verbose: Bool, + header: String, + isColorized: Bool + ) -> any ProgressAnimationProtocol { + Self.dynamic( + stream: stream, + verbose: verbose, + ttyTerminalAnimationFactory: { RedrawingPercentProgressAnimation( + terminal: $0, + header: header, + isColorized: isColorized + ) }, + dumbTerminalAnimationFactory: { SingleLinePercentProgressAnimation(stream: stream, header: header) }, + defaultAnimationFactory: { MultiLinePercentProgressAnimation(stream: stream, header: header) } + ) + } +} + +/// A redrawing lit-like progress animation. +final class RedrawingPercentProgressAnimation: ProgressAnimationProtocol { + private let terminal: TerminalController + private let header: String + private let isColorized: Bool + private var hasDisplayedHeader = false + + init(terminal: TerminalController, header: String, isColorized: Bool) { + self.terminal = terminal + self.header = header + self.isColorized = isColorized + } + + /// Creates repeating string for count times. + /// If count is negative, returns empty string. + private func repeating(string: String, count: Int) -> String { + return String(repeating: string, count: max(count, 0)) + } + + func colorizeText(color: TerminalController.Color = .noColor) -> TerminalController.Color { + if self.isColorized { + return color + } + return .noColor + } + + func update(step: Int, total: Int, text: String) { + assert(step <= total) + let isBold = self.isColorized + + let width = terminal.width + + if !hasDisplayedHeader { + let spaceCount = width / 2 - header.utf8.count / 2 + terminal.write(repeating(string: " ", count: spaceCount)) + terminal.write(header, inColor: colorizeText(color: .cyan), bold: isBold) + terminal.endLine() + hasDisplayedHeader = true + } else { + terminal.moveCursor(up: 1) + } + + terminal.clearLine() + let percentage = step * 100 / total + let paddedPercentage = percentage < 10 ? " \(percentage)" : "\(percentage)" + let prefix = "\(paddedPercentage)% " + terminal + .wrap("[", inColor: colorizeText(color: .green), bold: isBold) + terminal.write(prefix) + + let barWidth = width - prefix.utf8.count + let n = Int(Double(barWidth) * Double(percentage) / 100.0) + + terminal.write( + repeating(string: "=", count: n) + repeating(string: "-", count: barWidth - n), + inColor: colorizeText(color: .green) + ) + terminal.write("]", inColor: colorizeText(color: .green), bold: isBold) + terminal.endLine() + + terminal.clearLine() + if text.utf8.count > width { + let prefix = "…" + terminal.write(prefix) + terminal.write(String(text.suffix(width - prefix.utf8.count))) + } else { + terminal.write(text) + } + } + + func complete(success: Bool) { + terminal.endLine() + terminal.endLine() + } + + func clear() { + terminal.clearLine() + terminal.moveCursor(up: 1) + terminal.clearLine() + } +} + +/// A multi-line percent-based progress animation. +final class MultiLinePercentProgressAnimation: ProgressAnimationProtocol { + private struct Info: Equatable { + let percentage: Int + let text: String + } + + private let stream: WritableByteStream + private let header: String + private var hasDisplayedHeader = false + private var lastDisplayedText: String? = nil + + init(stream: WritableByteStream, header: String) { + self.stream = stream + self.header = header + } + + func update(step: Int, total: Int, text: String) { + assert(step <= total) + + if !hasDisplayedHeader, !header.isEmpty { + stream.send(header) + stream.send("\n") + stream.flush() + hasDisplayedHeader = true + } + + let percentage = step * 100 / total + stream.send("\(percentage)%: ").send(text) + stream.send("\n") + stream.flush() + lastDisplayedText = text + } + + func complete(success: Bool) {} + + func clear() {} +} diff --git a/Sources/Basics/ProgressAnimation/ProgressAnimationProtocol.swift b/Sources/Basics/ProgressAnimation/ProgressAnimationProtocol.swift new file mode 100644 index 00000000000..2a596271c8c --- /dev/null +++ b/Sources/Basics/ProgressAnimation/ProgressAnimationProtocol.swift @@ -0,0 +1,59 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2014-2024 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 class TSCBasic.TerminalController +import class TSCBasic.LocalFileOutputByteStream +import protocol TSCBasic.WritableByteStream +import protocol TSCUtility.ProgressAnimationProtocol + +@_spi(SwiftPMInternal) +public typealias ProgressAnimationProtocol = TSCUtility.ProgressAnimationProtocol + +/// Namespace to nest public progress animations under. +@_spi(SwiftPMInternal) +public enum ProgressAnimation { + /// Dynamically create a progress animation based on the current stream + /// capabilities and desired verbosity. + /// + /// - Parameters: + /// - stream: A stream to write animations into. + /// - verbose: The verbosity level of other output in the system. + /// - ttyTerminalAnimationFactory: A progress animation to use when the + /// output stream is connected to a terminal with support for special + /// escape sequences. + /// - dumbTerminalAnimationFactory: A progress animation to use when the + /// output stream is connected to a terminal without support for special + /// escape sequences for clearing lines or controlling cursor positions. + /// - defaultAnimationFactory: A progress animation to use when the + /// desired output is verbose or the output stream verbose or is not + /// connected to a terminal, e.g. a pipe or file. + /// - Returns: A progress animation instance matching the stream + /// capabilities and desired verbosity. + static func dynamic( + stream: WritableByteStream, + verbose: Bool, + ttyTerminalAnimationFactory: (TerminalController) -> any ProgressAnimationProtocol, + dumbTerminalAnimationFactory: () -> any ProgressAnimationProtocol, + defaultAnimationFactory: () -> any ProgressAnimationProtocol + ) -> any ProgressAnimationProtocol { + if let terminal = TerminalController(stream: stream), !verbose { + return ttyTerminalAnimationFactory(terminal) + } else if let fileStream = stream as? LocalFileOutputByteStream, + TerminalController.terminalType(fileStream) == .dumb + { + return dumbTerminalAnimationFactory() + } else { + return defaultAnimationFactory() + } + } +} + diff --git a/Sources/Basics/ProgressAnimation/SingleLinePercentProgressAnimation.swift b/Sources/Basics/ProgressAnimation/SingleLinePercentProgressAnimation.swift new file mode 100644 index 00000000000..c11b25e4b9b --- /dev/null +++ b/Sources/Basics/ProgressAnimation/SingleLinePercentProgressAnimation.swift @@ -0,0 +1,55 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2014-2024 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 class TSCBasic.TerminalController +import protocol TSCBasic.WritableByteStream + +/// A single line percent-based progress animation. +final class SingleLinePercentProgressAnimation: ProgressAnimationProtocol { + private let stream: WritableByteStream + private let header: String? + private var displayedPercentages: Set = [] + private var hasDisplayedHeader = false + + init(stream: WritableByteStream, header: String?) { + self.stream = stream + self.header = header + } + + func update(step: Int, total: Int, text: String) { + if let header = header, !hasDisplayedHeader { + stream.send(header) + stream.send("\n") + stream.flush() + hasDisplayedHeader = true + } + + let percentage = step * 100 / total + let roundedPercentage = Int(Double(percentage / 10).rounded(.down)) * 10 + if percentage != 100, !displayedPercentages.contains(roundedPercentage) { + stream.send(String(roundedPercentage)).send(".. ") + displayedPercentages.insert(roundedPercentage) + } + + stream.flush() + } + + func complete(success: Bool) { + if success { + stream.send("OK") + stream.flush() + } + } + + func clear() { + } +} diff --git a/Sources/Basics/ProgressAnimation/ThrottledProgressAnimation.swift b/Sources/Basics/ProgressAnimation/ThrottledProgressAnimation.swift new file mode 100644 index 00000000000..b5b3597f15c --- /dev/null +++ b/Sources/Basics/ProgressAnimation/ThrottledProgressAnimation.swift @@ -0,0 +1,85 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2024 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 _Concurrency +import TSCUtility + +/// A progress animation wrapper that throttles updates to a given interval. +final class ThrottledProgressAnimation: ProgressAnimationProtocol { + private let animation: ProgressAnimationProtocol + private let shouldUpdate: () -> Bool + private var pendingUpdate: (Int, Int, String)? + + init( + _ animation: ProgressAnimationProtocol, + now: @escaping () -> C.Instant, interval: C.Duration, clock: C.Type = C.self + ) { + self.animation = animation + var lastUpdate: C.Instant? + self.shouldUpdate = { + let now = now() + if let lastUpdate = lastUpdate, now < lastUpdate.advanced(by: interval) { + return false + } + // If we're over the interval or it's the first update, should update. + lastUpdate = now + return true + } + } + + func update(step: Int, total: Int, text: String) { + guard shouldUpdate() else { + pendingUpdate = (step, total, text) + return + } + pendingUpdate = nil + animation.update(step: step, total: total, text: text) + } + + func complete(success: Bool) { + if let (step, total, text) = pendingUpdate { + animation.update(step: step, total: total, text: text) + } + animation.complete(success: success) + } + + func clear() { + animation.clear() + } +} + +@_spi(SwiftPMInternal) +extension ProgressAnimationProtocol { + @_spi(SwiftPMInternal) + public func throttled( + now: @escaping () -> C.Instant, + interval: C.Duration, + clock: C.Type = C.self + ) -> some ProgressAnimationProtocol { + ThrottledProgressAnimation(self, now: now, interval: interval, clock: clock) + } + + @_spi(SwiftPMInternal) + public func throttled( + clock: C, + interval: C.Duration + ) -> some ProgressAnimationProtocol { + self.throttled(now: { clock.now }, interval: interval, clock: C.self) + } + + @_spi(SwiftPMInternal) + public func throttled( + interval: ContinuousClock.Duration + ) -> some ProgressAnimationProtocol { + self.throttled(clock: ContinuousClock(), interval: interval) + } +} diff --git a/Sources/Basics/SQLite.swift b/Sources/Basics/SQLite.swift index 31fbe3ab2fa..cab2374f3c3 100644 --- a/Sources/Basics/SQLite.swift +++ b/Sources/Basics/SQLite.swift @@ -10,21 +10,30 @@ // //===----------------------------------------------------------------------===// +import TSCBasic import Foundation +#if SWIFT_PACKAGE && (os(Windows) || os(Android)) +#if USE_IMPL_ONLY_IMPORTS +@_implementationOnly import SwiftToolchainCSQLite +#else +import SwiftToolchainCSQLite +#endif +#else #if USE_IMPL_ONLY_IMPORTS @_implementationOnly import SPMSQLite3 #else import SPMSQLite3 #endif +#endif /// A minimal SQLite wrapper. -public final class SQLite { +package final class SQLite { /// The location of the database. - public let location: Location + package let location: Location /// The configuration for the database. - public let configuration: Configuration + package let configuration: Configuration /// Pointer to the database. let db: OpaquePointer @@ -32,7 +41,7 @@ public final class SQLite { /// Create or open the database at the given path. /// /// The database is opened in serialized mode. - public init(location: Location, configuration: Configuration = Configuration()) throws { + package init(location: Location, configuration: Configuration = Configuration()) throws { self.location = location self.configuration = configuration @@ -64,19 +73,19 @@ public final class SQLite { } @available(*, deprecated, message: "use init(location:configuration) instead") - public convenience init(dbPath: AbsolutePath) throws { + package convenience init(dbPath: AbsolutePath) throws { try self.init(location: .path(dbPath)) } /// Prepare the given query. - public func prepare(query: String) throws -> PreparedStatement { + package func prepare(query: String) throws -> PreparedStatement { try PreparedStatement(db: self.db, query: query) } /// Directly execute the given query. /// /// Note: Use withCString for string arguments. - public func exec(query queryString: String, args: [CVarArg] = [], _ callback: SQLiteExecCallback? = nil) throws { + package func exec(query queryString: String, args: [CVarArg] = [], _ callback: SQLiteExecCallback? = nil) throws { let query = withVaList(args) { ptr in sqlite3_vmprintf(queryString, ptr) } @@ -96,27 +105,27 @@ public final class SQLite { } } - public func close() throws { + package func close() throws { try Self.checkError { sqlite3_close(db) } } - public typealias SQLiteExecCallback = ([Column]) -> Void + package typealias SQLiteExecCallback = ([Column]) -> Void - public struct Configuration { - public var busyTimeoutMilliseconds: Int32 - public var maxSizeInBytes: Int? + package struct Configuration { + package var busyTimeoutMilliseconds: Int32 + package var maxSizeInBytes: Int? // https://www.sqlite.org/pgszchng2016.html private let defaultPageSizeInBytes = 1024 - public init() { + package init() { self.busyTimeoutMilliseconds = 5000 self.maxSizeInBytes = .none } // FIXME: deprecated 12/2020, remove once clients migrated over @available(*, deprecated, message: "use busyTimeout instead") - public var busyTimeoutSeconds: Int32 { + package var busyTimeoutSeconds: Int32 { get { self._busyTimeoutSeconds } set { @@ -133,7 +142,7 @@ public final class SQLite { } } - public var maxSizeInMegabytes: Int? { + package var maxSizeInMegabytes: Int? { get { self.maxSizeInBytes.map { $0 / (1024 * 1024) } } @@ -142,12 +151,12 @@ public final class SQLite { } } - public var maxPageCount: Int? { + package var maxPageCount: Int? { self.maxSizeInBytes.map { $0 / self.defaultPageSizeInBytes } } } - public enum Location { + package enum Location: Sendable { case path(AbsolutePath) case memory case temporary @@ -165,7 +174,7 @@ public final class SQLite { } /// Represents an sqlite value. - public enum SQLiteValue { + package enum SQLiteValue { case null case string(String) case int(Int) @@ -173,35 +182,35 @@ public final class SQLite { } /// Represents a row returned by called step() on a prepared statement. - public struct Row { + package struct Row { /// The pointer to the prepared statement. let stmt: OpaquePointer /// Get integer at the given column index. - public func int(at index: Int32) -> Int { + package func int(at index: Int32) -> Int { Int(sqlite3_column_int64(self.stmt, index)) } /// Get blob data at the given column index. - public func blob(at index: Int32) -> Data { + package func blob(at index: Int32) -> Data { let bytes = sqlite3_column_blob(stmt, index)! let count = sqlite3_column_bytes(stmt, index) return Data(bytes: bytes, count: Int(count)) } /// Get string at the given column index. - public func string(at index: Int32) -> String { + package func string(at index: Int32) -> String { String(cString: sqlite3_column_text(self.stmt, index)) } } - public struct Column { - public var name: String - public var value: String + package struct Column { + package var name: String + package var value: String } /// Represents a prepared statement. - public struct PreparedStatement { + package struct PreparedStatement { typealias sqlite3_destructor_type = @convention(c) (UnsafeMutableRawPointer?) -> Void static let SQLITE_STATIC = unsafeBitCast(0, to: sqlite3_destructor_type.self) static let SQLITE_TRANSIENT = unsafeBitCast(-1, to: sqlite3_destructor_type.self) @@ -209,7 +218,7 @@ public final class SQLite { /// The pointer to the prepared statement. let stmt: OpaquePointer - public init(db: OpaquePointer, query: String) throws { + package init(db: OpaquePointer, query: String) throws { var stmt: OpaquePointer? try SQLite.checkError { sqlite3_prepare_v2(db, query, -1, &stmt, nil) } self.stmt = stmt! @@ -217,7 +226,7 @@ public final class SQLite { /// Evaluate the prepared statement. @discardableResult - public func step() throws -> Row? { + package func step() throws -> Row? { let result = sqlite3_step(stmt) switch result { @@ -231,7 +240,7 @@ public final class SQLite { } /// Bind the given arguments to the statement. - public func bind(_ arguments: [SQLiteValue]) throws { + package func bind(_ arguments: [SQLiteValue]) throws { for (idx, argument) in arguments.enumerated() { let idx = Int32(idx) + 1 switch argument { @@ -258,17 +267,17 @@ public final class SQLite { } /// Reset the prepared statement. - public func reset() throws { + package func reset() throws { try SQLite.checkError { sqlite3_reset(stmt) } } - /// Clear bindings from the prepared statment. - public func clearBindings() throws { + /// Clear bindings from the prepared statement. + package func clearBindings() throws { try SQLite.checkError { sqlite3_clear_bindings(stmt) } } /// Finalize the statement and free up resources. - public func finalize() throws { + package func finalize() throws { try SQLite.checkError { sqlite3_finalize(stmt) } } } @@ -296,7 +305,7 @@ public final class SQLite { } } - public enum Errors: Error { + package enum Errors: Error { case databaseFull } } diff --git a/Sources/Basics/SQLiteBackedCache.swift b/Sources/Basics/SQLiteBackedCache.swift index 73554819747..67e57854e58 100644 --- a/Sources/Basics/SQLiteBackedCache.swift +++ b/Sources/Basics/SQLiteBackedCache.swift @@ -13,17 +13,16 @@ import Foundation import protocol TSCBasic.Closable -import class TSCBasic.InMemoryFileSystem import var TSCBasic.localFileSystem /// SQLite backed persistent cache. -public final class SQLiteBackedCache: Closable { - public typealias Key = String +package final class SQLiteBackedCache: Closable { + package typealias Key = String - public let tableName: String - public let fileSystem: FileSystem - public let location: SQLite.Location - public let configuration: SQLiteBackedCacheConfiguration + package let tableName: String + package let fileSystem: FileSystem + package let location: SQLite.Location + package let configuration: SQLiteBackedCacheConfiguration private var state = State.idle private let stateLock = NSLock() @@ -37,7 +36,7 @@ public final class SQLiteBackedCache: Closable { /// - tableName: The SQLite table name. Must follow SQLite naming rules (e.g., no spaces). /// - location: SQLite.Location /// - configuration: Optional. Configuration for the cache. - public init(tableName: String, location: SQLite.Location, configuration: SQLiteBackedCacheConfiguration = .init()) { + package init(tableName: String, location: SQLite.Location, configuration: SQLiteBackedCacheConfiguration = .init()) { self.tableName = tableName self.location = location switch self.location { @@ -57,7 +56,7 @@ public final class SQLiteBackedCache: Closable { /// - tableName: The SQLite table name. Must follow SQLite naming rules (e.g., no spaces). /// - path: The path of the SQLite database. /// - configuration: Optional. Configuration for the cache. - public convenience init( + package convenience init( tableName: String, path: AbsolutePath, configuration: SQLiteBackedCacheConfiguration = .init() @@ -75,7 +74,7 @@ public final class SQLiteBackedCache: Closable { } } - public func close() throws { + package func close() throws { try self.withStateLock { if case .connected(let db) = self.state { try db.close() @@ -84,8 +83,8 @@ public final class SQLiteBackedCache: Closable { } } - public func put( - key: Key, + private func put( + rawKey key: SQLite.SQLiteValue, value: Value, replace: Bool = false, observabilityScope: ObservabilityScope? = nil @@ -95,7 +94,7 @@ public final class SQLiteBackedCache: Closable { try self.executeStatement(query) { statement in let data = try self.jsonEncoder.encode(value) let bindings: [SQLite.SQLiteValue] = [ - .string(key), + key, .blob(data), ] try statement.bind(bindings) @@ -107,18 +106,40 @@ public final class SQLiteBackedCache: Closable { } observabilityScope? .emit( - warning: "truncating \(self.tableName) cache database since it reached max size of \(self.configuration.maxSizeInBytes ?? 0) bytes" + warning: """ + truncating \(self.tableName) cache database since it reached max size of \( + self.configuration.maxSizeInBytes ?? 0 + ) bytes + """ ) try self.executeStatement("DELETE FROM \(self.tableName);") { statement in try statement.step() } - try self.put(key: key, value: value, replace: replace, observabilityScope: observabilityScope) + try self.put(rawKey: key, value: value, replace: replace, observabilityScope: observabilityScope) } catch { throw error } } - public func get(key: Key) throws -> Value? { + package func put( + blobKey key: some Sequence, + value: Value, + replace: Bool = false, + observabilityScope: ObservabilityScope? = nil + ) throws { + try self.put(rawKey: .blob(Data(key)), value: value, observabilityScope: observabilityScope) + } + + package func put( + key: Key, + value: Value, + replace: Bool = false, + observabilityScope: ObservabilityScope? = nil + ) throws { + try self.put(rawKey: .string(key), value: value, replace: replace, observabilityScope: observabilityScope) + } + + package func get(key: Key) throws -> Value? { let query = "SELECT value FROM \(self.tableName) WHERE key = ? LIMIT 1;" return try self.executeStatement(query) { statement -> Value? in try statement.bind([.string(key)]) @@ -129,7 +150,18 @@ public final class SQLiteBackedCache: Closable { } } - public func remove(key: Key) throws { + package func get(blobKey key: some Sequence) throws -> Value? { + let query = "SELECT value FROM \(self.tableName) WHERE key = ? LIMIT 1;" + return try self.executeStatement(query) { statement -> Value? in + try statement.bind([.blob(Data(key))]) + let data = try statement.step()?.blob(at: 0) + return try data.flatMap { + try self.jsonDecoder.decode(Value.self, from: $0) + } + } + } + + package func remove(key: Key) throws { let query = "DELETE FROM \(self.tableName) WHERE key = ?;" try self.executeStatement(query) { statement in try statement.bind([.string(key)]) @@ -143,7 +175,7 @@ public final class SQLiteBackedCache: Closable { let result: Result let statement = try db.prepare(query: query) do { - result = .success(try body(statement)) + result = try .success(body(statement)) } catch { result = .failure(error) } @@ -206,7 +238,7 @@ public final class SQLiteBackedCache: Closable { switch self.location { case .path(let path): if !self.fileSystem.exists(path.parentDirectory) { - try self.fileSystem.createDirectory(path.parentDirectory) + try self.fileSystem.createDirectory(path.parentDirectory, recursive: true) } return try self.fileSystem.withLock(on: path, type: .exclusive, body) case .memory, .temporary: @@ -221,12 +253,12 @@ public final class SQLiteBackedCache: Closable { } } -public struct SQLiteBackedCacheConfiguration { - public var truncateWhenFull: Bool +package struct SQLiteBackedCacheConfiguration { + package var truncateWhenFull: Bool fileprivate var underlying: SQLite.Configuration - public init() { + package init() { self.underlying = .init() self.truncateWhenFull = true self.maxSizeInMegabytes = 100 @@ -234,7 +266,7 @@ public struct SQLiteBackedCacheConfiguration { self.busyTimeoutMilliseconds = 1000 } - public var maxSizeInMegabytes: Int? { + package var maxSizeInMegabytes: Int? { get { self.underlying.maxSizeInMegabytes } @@ -243,7 +275,7 @@ public struct SQLiteBackedCacheConfiguration { } } - public var maxSizeInBytes: Int? { + package var maxSizeInBytes: Int? { get { self.underlying.maxSizeInBytes } @@ -252,7 +284,7 @@ public struct SQLiteBackedCacheConfiguration { } } - public var busyTimeoutMilliseconds: Int32 { + package var busyTimeoutMilliseconds: Int32 { get { self.underlying.busyTimeoutMilliseconds } diff --git a/Sources/Basics/Sandbox.swift b/Sources/Basics/Sandbox.swift index f8a32d46874..f24636cac13 100644 --- a/Sources/Basics/Sandbox.swift +++ b/Sources/Basics/Sandbox.swift @@ -45,7 +45,7 @@ public enum Sandbox { /// - Parameters: /// - command: The command line to sandbox (including executable as first argument) /// - fileSystem: The file system instance to use. - /// - strictness: The basic strictness level of the standbox. + /// - strictness: The basic strictness level of the sandbox. /// - writableDirectories: Paths under which writing should be allowed, even if they would otherwise be read-only based on the strictness or paths in `readOnlyDirectories`. /// - readOnlyDirectories: Paths under which writing should be denied, even if they would have otherwise been allowed by the rules implied by the strictness level. public static func apply( @@ -133,6 +133,9 @@ fileprivate func macOSSandboxProfile( // This is needed to use the UniformTypeIdentifiers API. contents += "(allow mach-lookup (global-name \"com.apple.lsd.mapdb\"))\n" + // For downloadable Metal toolchain lookups. + contents += "(allow mach-lookup (global-name \"com.apple.mobileassetd.v2\"))\n" + if allowNetworkConnections.filter({ $0 != .none }).isEmpty == false { // this is used by the system for caching purposes and will lead to log spew if not allowed contents += "(allow file-write* (regex \"/Users/*/Library/Caches/*/Cache.db*\"))" @@ -188,11 +191,16 @@ fileprivate func macOSSandboxProfile( } // Optionally allow writing to temporary directories (a lot of use of Foundation requires this). else if strictness == .writableTemporaryDirectory { - // Add `subpath` expressions for the regular and the Foundation temporary directories. - for tmpDir in ["/tmp", NSTemporaryDirectory()] { - writableDirectoriesExpression += try [ - "(subpath \(resolveSymlinks(AbsolutePath(validating: tmpDir)).quotedAsSubpathForSandboxProfile))", - ] + var stableCacheDirectories: [AbsolutePath] = [] + // Add `subpath` expressions for the regular, Foundation and clang module cache temporary directories. + for tmpDir in (["/tmp"] + threadSafeDarwinCacheDirectories.map(\.pathString)) { + let resolved = try resolveSymlinks(AbsolutePath(validating: tmpDir)) + if !stableCacheDirectories.contains(where: { $0.isAncestorOfOrEqual(to: resolved) }) { + stableCacheDirectories.append(resolved) + writableDirectoriesExpression += [ + "(subpath \(resolved.quotedAsSubpathForSandboxProfile))", + ] + } } } @@ -217,9 +225,20 @@ fileprivate func macOSSandboxProfile( // Emit rules for paths under which writing is allowed, even if they are descendants directories that are otherwise read-only. if writableDirectories.count > 0 { contents += "(allow file-write*\n" - // For any explicit writable directories, also include the relevant item replacement directories so that Foundation APIs using atomic writes are not blocked by the sandbox. - for path in writableDirectories + Set(writableDirectories.compactMap { try? fileSystem.itemReplacementDirectories(for: $0) }.flatMap { $0}) { + var stableItemReplacementDirectories: [AbsolutePath] = [] + for path in writableDirectories { contents += " (subpath \(try resolveSymlinks(path).quotedAsSubpathForSandboxProfile))\n" + + // `itemReplacementDirectories` may return a combination of stable directory paths, and subdirectories which are unique on every call. Avoid including unnecessary subdirectories in the Sandbox profile which may lead to nondeterminism in its construction. + if let itemReplacementDirectories = try? fileSystem.itemReplacementDirectories(for: path).sorted(by: { $0.pathString.count < $1.pathString.count }) { + for directory in itemReplacementDirectories { + let resolved = try resolveSymlinks(directory) + if !stableItemReplacementDirectories.contains(where: { $0.isAncestorOfOrEqual(to: resolved) }) { + stableItemReplacementDirectories.append(resolved) + contents += " (subpath \(resolved.quotedAsSubpathForSandboxProfile))\n" + } + } + } } contents += ")\n" } @@ -231,8 +250,8 @@ extension AbsolutePath { /// Private computed property that returns a version of the path as a string quoted for use as a subpath in a .sb sandbox profile. fileprivate var quotedAsSubpathForSandboxProfile: String { "\"" + self.pathString - .replacingOccurrences(of: "\\", with: "\\\\") - .replacingOccurrences(of: "\"", with: "\\\"") + .replacing("\\", with: "\\\\") + .replacing("\"", with: "\\\"") + "\"" } } diff --git a/Sources/Basics/SendableTimeInterval.swift b/Sources/Basics/SendableTimeInterval.swift index 0e079988922..3eb8286f47c 100644 --- a/Sources/Basics/SendableTimeInterval.swift +++ b/Sources/Basics/SendableTimeInterval.swift @@ -12,8 +12,6 @@ import enum Dispatch.DispatchTimeInterval -extension DispatchTimeInterval: @unchecked Sendable {} - /// This typealias hides `DispatchTimeInterval` as an implementation detail until we can use `Swift.Duration`, as the /// latter requires macOS 13. public typealias SendableTimeInterval = DispatchTimeInterval diff --git a/Sources/Basics/Serialization/SerializedJSON.swift b/Sources/Basics/Serialization/SerializedJSON.swift index 422af737418..a3e09ab16f1 100644 --- a/Sources/Basics/Serialization/SerializedJSON.swift +++ b/Sources/Basics/Serialization/SerializedJSON.swift @@ -31,7 +31,7 @@ extension SerializedJSON: ExpressibleByStringInterpolation { fileprivate var value: String = "" private func escape(_ string: String) -> String { - string.replacingOccurrences(of: #"\"#, with: #"\\"#) + string.replacing(#"\"#, with: #"\\"#) } public init(literalCapacity: Int, interpolationCount: Int) { diff --git a/Sources/Basics/SwiftVersion.swift b/Sources/Basics/SwiftVersion.swift index 0d479c4cdb5..6b0f6599882 100644 --- a/Sources/Basics/SwiftVersion.swift +++ b/Sources/Basics/SwiftVersion.swift @@ -13,10 +13,10 @@ #if USE_IMPL_ONLY_IMPORTS @_implementationOnly import TSCclibc #else -import TSCclibc +private import TSCclibc #endif -public struct SwiftVersion { +public struct SwiftVersion: Sendable { /// The version number. public var version: (major: Int, minor: Int, patch: Int) @@ -58,7 +58,7 @@ public struct SwiftVersion { extension SwiftVersion { /// The current version of the package manager. public static let current = SwiftVersion( - version: (5, 11, 0), + version: (6, 3, 0), isDevelopment: true, buildIdentifier: getBuildIdentifier() ) diff --git a/Sources/Basics/TestingLibrary.swift b/Sources/Basics/TestingLibrary.swift new file mode 100644 index 00000000000..289774ca673 --- /dev/null +++ b/Sources/Basics/TestingLibrary.swift @@ -0,0 +1,33 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2014-2024 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 +// +//===----------------------------------------------------------------------===// + +/// The testing libraries supported by the package manager. +public enum TestingLibrary: Sendable, CustomStringConvertible { + /// The XCTest library. + /// + /// This case represents both the open-source swift-corelibs-xctest + /// package and Apple's XCTest framework that ships with Xcode. + case xctest + + /// The swift-testing library. + case swiftTesting + + public var description: String { + switch self { + case .xctest: + "XCTest" + case .swiftTesting: + "Swift Testing" + } + } +} + diff --git a/Sources/Basics/Triple+Basics.swift b/Sources/Basics/Triple+Basics.swift index a7664bd39b9..e1f33bd31dc 100644 --- a/Sources/Basics/Triple+Basics.swift +++ b/Sources/Basics/Triple+Basics.swift @@ -10,8 +10,8 @@ // //===----------------------------------------------------------------------===// +import TSCUtility import enum TSCBasic.JSON -import class TSCBasic.Process extension Triple { public init(_ description: String) throws { @@ -24,6 +24,10 @@ extension Triple { } extension Triple { + public var isWasm: Bool { + [.wasm32, .wasm64].contains(self.arch) + } + public func isApple() -> Bool { vendor == .apple } @@ -59,6 +63,10 @@ extension Triple { os == .openbsd } + public func isFreeBSD() -> Bool { + os == .freebsd + } + /// Returns the triple string for the given platform version. /// /// This is currently meant for Apple platforms only. @@ -77,11 +85,20 @@ extension Triple { } /// Determine the versioned host triple using the Swift compiler. - public static func getHostTriple(usingSwiftCompiler swiftCompiler: AbsolutePath) throws -> Triple { + public static func getVersionedHostTriple(usingSwiftCompiler swiftCompiler: AbsolutePath) throws -> Triple { + try Self.getHostTriple(usingSwiftCompiler: swiftCompiler).versionedTriple + } + + /// Determine the unversioned host triple using the Swift compiler. + public static func getUnversionedHostTriple(usingSwiftCompiler swiftCompiler: AbsolutePath) throws -> Triple { + try Self.getHostTriple(usingSwiftCompiler: swiftCompiler).unversionedTriple + } + + public static func getHostTriple(usingSwiftCompiler swiftCompiler: AbsolutePath) throws -> (versionedTriple: Triple, unversionedTriple: Triple) { // Call the compiler to get the target info JSON. let compilerOutput: String do { - let result = try Process.popen(args: swiftCompiler.pathString, "-print-target-info") + let result = try AsyncProcess.popen(args: swiftCompiler.pathString, "-print-target-info") compilerOutput = try result.utf8Output().spm_chomp() } catch { throw InternalError("Failed to get target info (\(error.interpolationDescription))") @@ -96,9 +113,11 @@ extension Triple { ) } // Get the triple string from the parsed JSON. - let tripleString: String + let versionedTripleString: String + let unversionedTripleString: String do { - tripleString = try parsedTargetInfo.get("target").get("triple") + versionedTripleString = try parsedTargetInfo.get("target").get("triple") + unversionedTripleString = try parsedTargetInfo.get("target").get("unversionedTriple") } catch { throw InternalError( "Target info does not contain a triple string (\(error.interpolationDescription)).\nTarget info: \(parsedTargetInfo)" @@ -107,10 +126,10 @@ extension Triple { // Parse the triple string. do { - return try Triple(tripleString) + return try (Triple(versionedTripleString), Triple(unversionedTripleString)) } catch { throw InternalError( - "Failed to parse triple string (\(error.interpolationDescription)).\nTriple string: \(tripleString)" + "Failed to parse triple string (\(error.interpolationDescription)).\nVersioned triple string: \(versionedTripleString), unversioned triple string \(unversionedTripleString)" ) } } @@ -130,24 +149,45 @@ extension Triple { /// The file extension for dynamic libraries (eg. `.dll`, `.so`, or `.dylib`) public var dynamicLibraryExtension: String { guard let os = self.os else { - fatalError("Cannot create dynamic libraries unknown os.") + fatalError("Cannot create dynamic libraries for unknown os.") } switch os { case _ where isDarwin(): return ".dylib" - case .linux, .openbsd: + case .linux, .openbsd, .freebsd: return ".so" case .win32: return ".dll" case .wasi: return ".wasm" default: - fatalError("Cannot create dynamic libraries for os \"\(os)\".") + break + } + + guard let objectFormat = self.objectFormat else { + fatalError("Cannot create dynamic libraries for unknown object format.") + } + + switch objectFormat { + case .coff: + return ".coff" + case .elf: + return ".elf" + case .macho: + return ".macho" + case .wasm: + return ".wasm" + case .xcoff: + return ".xcoff" } } public var executableExtension: String { + guard !self.isWasm else { + return ".wasm" + } + guard let os = self.os else { return "" } @@ -155,10 +195,8 @@ extension Triple { switch os { case _ where isDarwin(): return "" - case .linux, .openbsd: + case .linux, .openbsd, .freebsd: return "" - case .wasi: - return ".wasm" case .win32: return ".exe" case .noneOS: diff --git a/Sources/Basics/URL.swift b/Sources/Basics/URL.swift new file mode 100644 index 00000000000..def99447eb4 --- /dev/null +++ b/Sources/Basics/URL.swift @@ -0,0 +1,38 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2021 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 struct Foundation.URL + +extension URL { + /// Returns the path of the file URL. + /// + /// This should always be used whenever the file path equivalent of a URL is needed. DO NOT use ``path`` or ``path(percentEncoded:)``, as these deal in terms of the path portion of the URL representation per RFC8089, which on Windows would include a leading slash. + /// + /// - throws: ``FileURLError`` if the URL does not represent a file or its path is otherwise not representable. + public var filePath: AbsolutePath { + get throws { + guard isFileURL else { + throw FileURLError.notRepresentable(self) + } + return try withUnsafeFileSystemRepresentation { cString in + guard let cString else { + throw FileURLError.notRepresentable(self) + } + return try AbsolutePath(validating: String(cString: cString)) + } + } + } +} + +fileprivate enum FileURLError: Error { + case notRepresentable(URL) +} diff --git a/Sources/Basics/Vendor/Triple+Platforms.swift b/Sources/Basics/Vendor/Triple+Platforms.swift index 33a2558ac54..d26931b0ca7 100644 --- a/Sources/Basics/Vendor/Triple+Platforms.swift +++ b/Sources/Basics/Vendor/Triple+Platforms.swift @@ -244,7 +244,7 @@ extension Triple { precondition(self.os?.isDarwin ?? false) switch darwinPlatform! { case .macOS: - // The integrated driver falls back to `osVersion` for ivalid macOS + // The integrated driver falls back to `osVersion` for invalid macOS // versions, this decision might be worth revisiting. let macVersion = _macOSVersion ?? osVersion // The first deployment of arm64 for macOS is version 11 @@ -304,7 +304,7 @@ extension Triple { case .linux: return environment == .android ? "android" : "linux" - case .freeBSD: + case .freebsd: return "freebsd" case .openbsd: return "openbsd" @@ -350,9 +350,9 @@ extension Triple { /// `tripleVersion >= featureVersion`. /// /// - SeeAlso: `Triple.supports(_:)` - public struct FeatureAvailability { - - public enum Availability { +public struct FeatureAvailability: Sendable { + + public enum Availability: Sendable { case unavailable case available(since: Version) case availableInAllVersions diff --git a/Sources/Basics/Vendor/Triple.swift b/Sources/Basics/Vendor/Triple.swift index 7804ed26adf..441962f43ea 100644 --- a/Sources/Basics/Vendor/Triple.swift +++ b/Sources/Basics/Vendor/Triple.swift @@ -43,7 +43,7 @@ /// /// This is a port of https://github.com/apple/swift-llvm/blob/stable/include/llvm/ADT/Triple.h @dynamicMemberLookup -public struct Triple { +public struct Triple: Sendable { /// `Triple` proxies predicates from `Triple.OS`, returning `false` for an unknown OS. public subscript(dynamicMember predicate: KeyPath) -> Bool { os?[keyPath: predicate] ?? false @@ -71,7 +71,7 @@ public struct Triple { public let objectFormat: ObjectFormat? /// Represents a version that may be present in the target triple. - public struct Version: Equatable, Comparable, CustomStringConvertible { + public struct Version: Equatable, Comparable, CustomStringConvertible, Sendable { public static let zero = Version(0, 0, 0) public var major: Int @@ -426,7 +426,7 @@ extension Triple { } } - public enum Arch: String, CaseIterable { +public enum Arch: String, CaseIterable, Decodable, Sendable { /// ARM (little endian): arm, armv.*, xscale case arm // ARM (big endian): armeb @@ -439,7 +439,7 @@ extension Triple { case aarch64_be // AArch64 (little endian) ILP32: aarch64_32 case aarch64_32 - /// ARC: Synopsys ARC + /// ARC: Synopsis ARC case arc /// AVR: Atmel AVR microcontroller case avr @@ -844,9 +844,9 @@ extension Triple { // MARK: - Parse SubArch extension Triple { - public enum SubArch: Hashable { + public enum SubArch: Hashable, Sendable { - public enum ARM { + public enum ARM: Sendable { public enum Profile { case a, r, m @@ -916,13 +916,13 @@ extension Triple { } } - public enum Kalimba { + public enum Kalimba: Sendable { case v3 case v4 case v5 } - public enum MIPS { + public enum MIPS: Sendable { case r6 } @@ -1022,7 +1022,7 @@ extension Triple { // MARK: - Parse Vendor extension Triple { - public enum Vendor: String, CaseIterable, TripleComponent { + public enum Vendor: String, CaseIterable, TripleComponent, Sendable { case apple case pc case scei @@ -1084,12 +1084,12 @@ extension Triple { // MARK: - Parse OS extension Triple { - public enum OS: String, CaseIterable, TripleComponent { + public enum OS: String, CaseIterable, TripleComponent, Sendable { case ananas case cloudABI = "cloudabi" case darwin case dragonFly = "dragonfly" - case freeBSD = "freebsd" + case freebsd = "freebsd" case fuchsia case ios case kfreebsd @@ -1137,7 +1137,7 @@ extension Triple { case _ where os.hasPrefix("dragonfly"): return .dragonFly case _ where os.hasPrefix("freebsd"): - return .freeBSD + return .freebsd case _ where os.hasPrefix("fuchsia"): return .fuchsia case _ where os.hasPrefix("ios"): @@ -1258,7 +1258,7 @@ extension Triple { } } - public enum Environment: String, CaseIterable, Equatable { + public enum Environment: String, CaseIterable, Equatable, Sendable { case eabihf case eabi case elfv1 @@ -1354,7 +1354,7 @@ extension Triple { // MARK: - Parse Object Format extension Triple { - public enum ObjectFormat { + public enum ObjectFormat: Sendable { case coff case elf case macho diff --git a/Sources/BinarySymbols/CMakeLists.txt b/Sources/BinarySymbols/CMakeLists.txt new file mode 100644 index 00000000000..a82e40ae8fe --- /dev/null +++ b/Sources/BinarySymbols/CMakeLists.txt @@ -0,0 +1,19 @@ +# 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 Swift project authors + +add_library(BinarySymbols STATIC + ClangHostDefaultObjectsDetector.swift + LLVMObjdumpSymbolProvider.swift + ReferencedSymbols.swift + SymbolProvider.swift) +target_link_libraries(BinarySymbols PUBLIC + Basics) + +# NOTE(compnerd) workaround for CMake not setting up include flags yet +set_target_properties(BinarySymbols PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_Swift_MODULE_DIRECTORY}) diff --git a/Sources/BinarySymbols/ClangHostDefaultObjectsDetector.swift b/Sources/BinarySymbols/ClangHostDefaultObjectsDetector.swift new file mode 100644 index 00000000000..ef22c3968d6 --- /dev/null +++ b/Sources/BinarySymbols/ClangHostDefaultObjectsDetector.swift @@ -0,0 +1,141 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2014 - 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 Swift project authors + */ + +import Basics +import Foundation + +import protocol TSCBasic.WritableByteStream + +package func detectDefaultObjects( + clang: AbsolutePath, fileSystem: any FileSystem, hostTriple: Triple, + observabilityScope: ObservabilityScope +) async throws -> [AbsolutePath] { + let clangProcess = AsyncProcess(args: clang.pathString, "-###", "-x", "c", "-") + let stdinStream = try clangProcess.launch() + stdinStream.write( + #""" + #include + int main(int argc, char *argv[]) { + printf("Hello world!\n") + return 0; + } + """# + ) + stdinStream.flush() + try stdinStream.close() + let clangResult = try await clangProcess.waitUntilExit() + guard case .terminated(let status) = clangResult.exitStatus, + status == 0 + else { + throw StringError("Couldn't run clang on sample hello world program") + } + let commandsStrings = try clangResult.utf8stderrOutput().split(whereSeparator: \.isNewline) + + let commands = commandsStrings.map { $0.split(whereSeparator: \.isWhitespace) } + guard let linkerCommand = commands.last(where: { $0.first?.contains("ld") == true }) else { + throw StringError("Couldn't find default link command") + } + + // TODO: This logic doesn't support Darwin and Windows based, c.f. https://github.com/swiftlang/swift-package-manager/issues/8753 + let libraryExtensions = [hostTriple.staticLibraryExtension, hostTriple.dynamicLibraryExtension] + var objects: Set = [] + var searchPaths: [AbsolutePath] = [] + + var linkerArguments = linkerCommand.dropFirst().map { + $0.replacingOccurrences(of: "\"", with: "") + } + + if hostTriple.isLinux() { + // Some platform still separate those out... + linkerArguments.append(contentsOf: ["-lm", "-lpthread", "-ldl"]) + } + + func handleArgument(_ argument: String) throws { + if argument.hasPrefix("-L") { + searchPaths.append(try AbsolutePath(validating: String(argument.dropFirst(2)))) + } else if argument.hasPrefix("-l") && !argument.hasSuffix("lto_library") { + let libraryName = argument.dropFirst(2) + let potentialLibraries = searchPaths.flatMap { path in + return libraryExtensions.map { ext in + path.appending("\(hostTriple.dynamicLibraryPrefix)\(libraryName)\(ext)") + } + } + + guard let library = potentialLibraries.first(where: { fileSystem.isFile($0) }) else { + observabilityScope.emit(warning: "Could not find library: \(libraryName)") + return + } + + // Try and detect if this a GNU ld linker script. + if let fileContents = try fileSystem.readFileContents(library).validDescription { + let lines = fileContents.split(whereSeparator: \.isNewline) + guard lines.contains(where: { $0.contains("GNU ld script") }) else { + objects.insert(library) + return + } + + // If it is try and parse GROUP/INPUT commands as documented in https://sourceware.org/binutils/docs/ld/File-Commands.html + // Empirically it seems like GROUP is the only used such directive for libraries of interest. + // Empirically it looks like packaging linker scripts use spaces around parenthesis which greatly simplifies parsing. + let inputs = lines.filter { $0.hasPrefix("GROUP") || $0.hasPrefix("INPUT") } + let words = inputs.flatMap { $0.split(whereSeparator: \.isWhitespace) } + let newArguments = words.filter { + !["GROUP", "AS_NEEDED", "INPUT"].contains($0) && $0 != "(" && $0 != ")" + }.map(String.init) + + for arg in newArguments { + if arg.hasPrefix("-l") { + try handleArgument(arg) + } else { + // First try and locate the file relative to the linker script. + let siblingPath = try AbsolutePath( + validating: arg, + relativeTo: try AbsolutePath(validating: library.dirname)) + if fileSystem.isFile(siblingPath) { + try handleArgument(siblingPath.pathString) + } else { + // If this fails the file needs to be resolved relative to the search paths. + guard + let library = searchPaths.map({ $0.appending(arg) }).first(where: { + fileSystem.isFile($0) + }) + else { + observabilityScope.emit( + warning: + "Malformed linker script at \(library): found no library named \(arg)" + ) + continue + } + try handleArgument(library.pathString) + } + } + } + + } else { + objects.insert(library) + } + + } else if try argument.hasSuffix(".o") + && fileSystem.isFile(AbsolutePath(validating: argument)) + { + objects.insert(try AbsolutePath(validating: argument)) + } else if let dotIndex = argument.firstIndex(of: "."), + libraryExtensions.first(where: { argument[dotIndex...].contains($0) }) != nil + { + objects.insert(try AbsolutePath(validating: argument)) + } + } + + for argument in linkerArguments { + try handleArgument(argument) + } + + return objects.compactMap { $0 } +} diff --git a/Sources/BinarySymbols/LLVMObjdumpSymbolProvider.swift b/Sources/BinarySymbols/LLVMObjdumpSymbolProvider.swift new file mode 100644 index 00000000000..70db3c39e82 --- /dev/null +++ b/Sources/BinarySymbols/LLVMObjdumpSymbolProvider.swift @@ -0,0 +1,137 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2014 - 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 Swift project authors + */ + +import Basics +import RegexBuilder + +package struct LLVMObjdumpSymbolProvider: SymbolProvider { + private let objdumpPath: AbsolutePath + + package init(objdumpPath: AbsolutePath) { + self.objdumpPath = objdumpPath + } + + package func symbols(for binary: AbsolutePath, symbols: inout ReferencedSymbols, recordUndefined: Bool = true) async throws { + let objdumpProcess = AsyncProcess(args: objdumpPath.pathString, "-t", "-T", binary.pathString) + try objdumpProcess.launch() + let result = try await objdumpProcess.waitUntilExit() + guard case .terminated(let status) = result.exitStatus, + status == 0 else { + throw InternalError("Unable to run llvm-objdump") + } + + try parse(output: try result.utf8Output(), symbols: &symbols, recordUndefined: recordUndefined) + } + + package func parse(output: String, symbols: inout ReferencedSymbols, recordUndefined: Bool = true) throws { + let visibility = Reference() + let weakLinkage = Reference() + let section = Reference() + let name = Reference() + let symbolLineRegex = Regex { + Anchor.startOfLine + Repeat(CharacterClass.hexDigit, count: 16) // The address of the symbol + CharacterClass.whitespace + Capture(as: visibility) { + ChoiceOf { + "l" + "g" + "u" + "!" + " " + } + } + Capture(as: weakLinkage) { // Whether the symbol is weak or strong + ChoiceOf { + "w" + " " + } + } + ChoiceOf { + "C" + " " + } + ChoiceOf { + "W" + " " + } + ChoiceOf { + "I" + "i" + " " + } + ChoiceOf { + "D" + "d" + " " + } + ChoiceOf { + "F" + "f" + "O" + " " + } + OneOrMore{ + .whitespace + } + Capture(as: section) { // The section the symbol appears in + ZeroOrMore { + .whitespace.inverted + } + } + ZeroOrMore { + .anyNonNewline + } + CharacterClass.whitespace + Capture(as: name) { // The name of symbol + OneOrMore { + .whitespace.inverted + } + } + Anchor.endOfLine + } + for line in output.split(whereSeparator: \.isNewline) { + guard let match = try symbolLineRegex.wholeMatch(in: line) else { + // This isn't a symbol definition line + continue + } + + switch match[section] { + case "*UND*": + guard recordUndefined else { + continue + } + // Weak symbols are optional + if match[weakLinkage] != "w" { + symbols.addUndefined(String(match[name])) + } + default: + symbols.addDefined(String(match[name])) + } + } + } + + private func name(line: Substring) -> Substring? { + guard let lastspace = line.lastIndex(where: \.isWhitespace) else { return nil } + return line[line.index(after: lastspace)...] + } + + private func section(line: Substring) throws -> Substring { + guard line.count > 25 else { + throw InternalError("Unable to run llvm-objdump") + } + let sectionStart = line.index(line.startIndex, offsetBy: 25) + guard let sectionEnd = line[sectionStart...].firstIndex(where: \.isWhitespace) else { + throw InternalError("Unable to run llvm-objdump") + } + return line[sectionStart.. + package private(set) var undefined: Set + + package init() { + self.defined = [] + self.undefined = [] + } + + mutating func addUndefined(_ name: String) { + guard !self.defined.contains(name) else { + return + } + self.undefined.insert(name) + } + + mutating func addDefined(_ name: String) { + self.defined.insert(name) + self.undefined.remove(name) + } +} diff --git a/Sources/BinarySymbols/SymbolProvider.swift b/Sources/BinarySymbols/SymbolProvider.swift new file mode 100644 index 00000000000..29f4ee98c26 --- /dev/null +++ b/Sources/BinarySymbols/SymbolProvider.swift @@ -0,0 +1,21 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2014 - 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 Swift project authors + */ + +import Basics + +package protocol SymbolProvider { + func symbols(for: AbsolutePath, symbols: inout ReferencedSymbols, recordUndefined: Bool) async throws +} + +extension SymbolProvider { + package func symbols(for binary: AbsolutePath, symbols: inout ReferencedSymbols) async throws { + try await self.symbols(for: binary, symbols: &symbols, recordUndefined: true) + } +} diff --git a/Sources/Build/BuildDescription/ClangTargetBuildDescription.swift b/Sources/Build/BuildDescription/ClangModuleBuildDescription.swift similarity index 72% rename from Sources/Build/BuildDescription/ClangTargetBuildDescription.swift rename to Sources/Build/BuildDescription/ClangModuleBuildDescription.swift index b976a7807d1..4116e970b14 100644 --- a/Sources/Build/BuildDescription/ClangTargetBuildDescription.swift +++ b/Sources/Build/BuildDescription/ClangModuleBuildDescription.swift @@ -11,22 +11,28 @@ //===----------------------------------------------------------------------===// import Basics +import PackageGraph import PackageLoading import PackageModel -import struct PackageGraph.ResolvedTarget +import struct PackageGraph.ModulesGraph +import struct PackageGraph.ResolvedModule import struct SPMBuildCore.BuildParameters import struct SPMBuildCore.BuildToolPluginInvocationResult -import struct SPMBuildCore.PrebuildCommandResult +import struct SPMBuildCore.CommandPluginResult -import enum TSCBasic.ProcessEnv +@available(*, deprecated, renamed: "ClangModuleBuildDescription") +public typealias ClangTargetBuildDescription = ClangModuleBuildDescription + +/// Build description for a Clang target i.e. C language family module. +public final class ClangModuleBuildDescription { + /// The package this target belongs to. + public let package: ResolvedPackage -/// Target description for a Clang target i.e. C language family target. -public final class ClangTargetBuildDescription { /// The target described by this target. - public let target: ResolvedTarget + public let target: ResolvedModule /// The underlying clang target. - public let clangTarget: ClangTarget + public let clangTarget: ClangModule /// The tools version of the package that declared the target. This can /// can be used to conditionalize semantically significant changes in how @@ -36,6 +42,11 @@ public final class ClangTargetBuildDescription { /// The build parameters. let buildParameters: BuildParameters + /// The destination for while this module is built. + public var destination: BuildParameters.Destination { + self.buildParameters.destination + } + /// The build environment. var buildEnvironment: BuildEnvironment { buildParameters.buildEnvironment @@ -46,9 +57,19 @@ public final class ClangTargetBuildDescription { self.target.underlying.resources + self.pluginDerivedResources } + /// The list of files in the target that were marked as ignored. + public var ignored: [AbsolutePath] { + self.target.underlying.ignored + } + + /// The list of other kinds of files in the target. + public var others: [AbsolutePath] { + self.target.underlying.others + } + /// Path to the bundle generated for this module (if any). var bundlePath: AbsolutePath? { - guard !resources.isEmpty else { + guard !self.resources.isEmpty else { return .none } @@ -108,32 +129,34 @@ public final class ClangTargetBuildDescription { /// Create a new target description with target and build parameters. init( - target: ResolvedTarget, + package: ResolvedPackage, + target: ResolvedModule, toolsVersion: ToolsVersion, additionalFileRules: [FileRuleDescription] = [], buildParameters: BuildParameters, buildToolPluginInvocationResults: [BuildToolPluginInvocationResult] = [], - prebuildCommandResults: [PrebuildCommandResult] = [], + prebuildCommandResults: [CommandPluginResult] = [], fileSystem: FileSystem, observabilityScope: ObservabilityScope ) throws { - guard let clangTarget = target.underlying as? ClangTarget else { + guard let clangTarget = target.underlying as? ClangModule else { throw InternalError("underlying target type mismatch \(target)") } + self.package = package self.clangTarget = clangTarget self.fileSystem = fileSystem self.target = target self.toolsVersion = toolsVersion self.buildParameters = buildParameters - self.tempsPath = buildParameters.buildPath.appending(component: target.c99name + ".build") + self.tempsPath = target.tempsPath(buildParameters) self.derivedSources = Sources(paths: [], root: tempsPath.appending("DerivedSources")) // We did not use to apply package plugins to C-family targets in prior tools-versions, this preserves the behavior. if toolsVersion >= .v5_9 { self.buildToolPluginInvocationResults = buildToolPluginInvocationResults - (self.pluginDerivedSources, self.pluginDerivedResources) = SharedTargetBuildDescription.computePluginGeneratedFiles( + (self.pluginDerivedSources, self.pluginDerivedResources) = ModulesGraph.computePluginGeneratedFiles( target: target, toolsVersion: toolsVersion, additionalFileRules: additionalFileRules, @@ -180,7 +203,7 @@ public final class ClangTargetBuildDescription { } } - /// An array of tuple containing filename, source, object and dependency path for each of the source in this target. + /// An array of tuples containing filename, source, object and dependency path for each of the source in this target. public func compilePaths() throws -> [(filename: RelativePath, source: AbsolutePath, object: AbsolutePath, deps: AbsolutePath)] { @@ -200,6 +223,29 @@ public final class ClangTargetBuildDescription { } } + /// Determines the arguments needed to run `swift-symbolgraph-extract` for + /// this module. + package func symbolGraphExtractArguments() throws -> [String] { + var args = [String]() + + args += ["-module-name", self.target.c99name] + args += try self.buildParameters.tripleArgs(for: self.target) + args += ["-module-cache-path", try self.buildParameters.moduleCache.pathString] + + if self.clangTarget.isCXX { + args += ["-cxx-interoperability-mode=default"] + } + if let cxxLanguageStandard = self.clangTarget.cxxLanguageStandard { + args += ["-Xcc", "-std=\(cxxLanguageStandard)"] + } + args += ["-I", self.clangTarget.includeDir.pathString] + args += self.additionalFlags.asSwiftcCCompilerFlags() + // Unconditionally use clang modules with swift tools. + args += try self.clangModuleArguments().asSwiftcCCompilerFlags() + args += try self.currentModuleMapFileArguments().asSwiftcCCompilerFlags() + return args + } + /// Builds up basic compilation arguments for a source file in this target; these arguments may be different for C++ /// vs non-C++. /// NOTE: The parameter to specify whether to get C++ semantics is currently optional, but this is only for revlock @@ -207,7 +253,8 @@ public final class ClangTargetBuildDescription { /// default value (possibly based on the filename suffix). public func basicArguments( isCXX isCXXOverride: Bool? = .none, - isC: Bool = false + isC: Bool = false, + isAsm: Bool = false ) throws -> [String] { // For now fall back on the hold semantics if the C++ nature isn't specified. This is temporary until clients // have been updated. @@ -218,44 +265,31 @@ public final class ClangTargetBuildDescription { if self.buildParameters.triple.isDarwin() { args += ["-fobjc-arc"] } - args += try buildParameters.targetTripleArgs(for: target) + args += try self.buildParameters.tripleArgs(for: target) args += optimizationArguments args += activeCompilationConditions args += ["-fblocks"] - let buildTriple = self.buildParameters.triple // Enable index store, if appropriate. - // - // This feature is not widely available in OSS clang. So, we only enable - // index store for Apple's clang or if explicitly asked to. - if ProcessEnv.vars.keys.contains("SWIFTPM_ENABLE_CLANG_INDEX_STORE") { - args += self.buildParameters.indexStoreArguments(for: target) - } else if buildTriple.isDarwin(), - (try? self.buildParameters.toolchain._isClangCompilerVendorApple()) == true - { + if let supported = try? ClangSupport.supportsFeature( + name: "index-unit-output-path", + toolchain: self.buildParameters.toolchain + ), supported { args += self.buildParameters.indexStoreArguments(for: target) } // Enable Clang module flags, if appropriate. - let enableModules: Bool let triple = self.buildParameters.triple - if toolsVersion < .v5_8 { - // For version < 5.8, we enable them except in these cases: - // 1. on Darwin when compiling for C++, because C++ modules are disabled on Apple-built Clang releases - // 2. on Windows when compiling for any language, because of issues with the Windows SDK - // 3. on Android when compiling for any language, because of issues with the Android SDK - enableModules = !(triple.isDarwin() && isCXX) && !triple.isWindows() && !triple.isAndroid() - } else { - // For version >= 5.8, we disable them when compiling for C++ regardless of platforms, see: - // https://github.com/llvm/llvm-project/issues/55980 for clang frontend crash when module - // enabled for C++ on c++17 standard and above. - enableModules = !isCXX && !triple.isWindows() && !triple.isAndroid() - } - + // Swift is able to use modules on non-Darwin platforms because it injects its own module maps + // via vfs. However, nothing does that for C based compilation, and so non-Darwin platforms can't + // support clang modules. + // Note that if modules get enabled for other platforms later, they can't be used with C++ until + // https://github.com/llvm/llvm-project/issues/55980 (crash on C++17 and later) is fixed. + // clang modules aren't fully supported in C++ mode in the current Darwin SDKs. + let enableModules = triple.isDarwin() && !isCXX if enableModules { - // Using modules currently conflicts with the Windows and Android SDKs. - args += ["-fmodules", "-fmodule-name=" + target.c99name] + args += try self.clangModuleArguments() } // Only add the build path to the framework search path if there are binary frameworks to link against. @@ -265,9 +299,7 @@ public final class ClangTargetBuildDescription { args += ["-I", clangTarget.includeDir.pathString] args += additionalFlags - if enableModules { - args += try moduleCacheArgs - } + args += buildParameters.sanitizers.compileCFlags() // Add arguments from declared build settings. @@ -276,7 +308,7 @@ public final class ClangTargetBuildDescription { // Include the path to the resource header unless the arguments are // being evaluated for a C file. A C file cannot depend on the resource // accessor header due to it exporting a Foundation type (`NSBundle`). - if let resourceAccessorHeaderFile, !isC { + if let resourceAccessorHeaderFile, !isC && !isAsm { args += ["-include", resourceAccessorHeaderFile.pathString] } @@ -317,6 +349,34 @@ public final class ClangTargetBuildDescription { args += ["-I", includeSearchPath.pathString] } + // FIXME: Remove this once it becomes possible to express this dependency in a package manifest. + // + // On Linux/Android swift-corelibs-foundation depends on dispatch library which is + // currently shipped with the Swift toolchain. + if (triple.isLinux() || triple.isAndroid()) && self.package.id == .plain("swift-corelibs-foundation") { + let swiftCompilerPath = self.buildParameters.toolchain.swiftCompilerPath + let toolchainResourcesPath = swiftCompilerPath.parentDirectory + .parentDirectory + .appending(components: ["lib", "swift"]) + args += ["-I", toolchainResourcesPath.pathString] + } + + // suppress warnings if the package is remote + if self.package.isRemote { + // `-w` (suppress warnings) and the other warning control flags are mutually exclusive + args = args.filter { arg in + // we consider the following flags: + // -Wxxxx + // -Wno-xxxx + // -Werror + // -Werror=xxxx + // -Wno-error + // -Wno-error=xxxx + arg.count <= 2 || !arg.starts(with: "-W") + } + args += ["-w"] + } + return args } @@ -335,8 +395,9 @@ public final class ClangTargetBuildDescription { let isCXX = path.source.extension.map { SupportedLanguageExtension.cppExtensions.contains($0) } ?? false let isC = path.source.extension.map { $0 == SupportedLanguageExtension.c.rawValue } ?? false + let isAsm = path.source.extension.map { SupportedLanguageExtension.assemblyExtensions.contains($0) } ?? false - var args = try basicArguments(isCXX: isCXX, isC: isC) + var args = try basicArguments(isCXX: isCXX, isC: isC, isAsm: isAsm) args += ["-MD", "-MT", "dependencies", "-MF", path.deps.pathString] @@ -404,11 +465,22 @@ public final class ClangTargetBuildDescription { return compilationConditions } - /// Module cache arguments. - private var moduleCacheArgs: [String] { - get throws { - try ["-fmodules-cache-path=\(buildParameters.moduleCache.pathString)"] + /// Enable Clang module flags. + private func clangModuleArguments() throws -> [String] { + let cachePath = try self.buildParameters.moduleCache.pathString + return [ + "-fmodules", + "-fmodule-name=\(self.target.c99name)", + "-fmodules-cache-path=\(cachePath)", + ] + } + + private func currentModuleMapFileArguments() throws -> [String] { + // Pass the path to the current module's module map if present. + if let moduleMap = self.moduleMap { + return ["-fmodule-map-file=\(moduleMap.pathString)"] } + return [] } /// Generate the resource bundle accessor, if appropriate. @@ -449,6 +521,7 @@ public final class ClangTargetBuildDescription { let headerContent = """ + #if __OBJC__ #import #if __cplusplus @@ -462,6 +535,7 @@ public final class ClangTargetBuildDescription { #if __cplusplus } #endif + #endif """ let headerFile = derivedSources.root.appending("resource_bundle_accessor.h") @@ -473,3 +547,17 @@ public final class ClangTargetBuildDescription { ) } } + +extension ClangModuleBuildDescription { + package func dependencies( + using plan: BuildPlan + ) -> [ModuleBuildDescription.Dependency] { + ModuleBuildDescription.clang(self).dependencies(using: plan) + } + + package func recursiveDependencies( + using plan: BuildPlan + ) -> [ModuleBuildDescription.Dependency] { + ModuleBuildDescription.clang(self).recursiveDependencies(using: plan) + } +} diff --git a/Sources/Build/BuildDescription/ModuleBuildDescription.swift b/Sources/Build/BuildDescription/ModuleBuildDescription.swift new file mode 100644 index 00000000000..8c3713567f1 --- /dev/null +++ b/Sources/Build/BuildDescription/ModuleBuildDescription.swift @@ -0,0 +1,213 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2015-2023 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 Basics +import struct PackageGraph.ResolvedModule +import struct PackageGraph.ResolvedPackage +import struct PackageGraph.ResolvedProduct +import struct PackageModel.Resource +import struct PackageModel.ToolsVersion +import struct SPMBuildCore.BuildToolPluginInvocationResult +import struct SPMBuildCore.BuildParameters +import protocol SPMBuildCore.ModuleBuildDescription + +public enum BuildDescriptionError: Swift.Error { + case requestedFileNotPartOfTarget(targetName: String, requestedFilePath: AbsolutePath) +} + +@available(*, deprecated, renamed: "ModuleBuildDescription") +public typealias TargetBuildDescription = ModuleBuildDescription + +/// A module build description which can either be for a Swift or Clang module. +public enum ModuleBuildDescription: SPMBuildCore.ModuleBuildDescription { + /// Swift target description. + case swift(SwiftModuleBuildDescription) + + /// Clang target description. + case clang(ClangModuleBuildDescription) + + /// The objects in this target. + var objects: [AbsolutePath] { + get throws { + switch self { + case .swift(let module): + return try module.objects + case .clang(let module): + return try module.objects + } + } + } + + /// The resources in this target. + var resources: [Resource] { + switch self { + case .swift(let buildDescription): + return buildDescription.resources + case .clang(let buildDescription): + return buildDescription.resources + } + } + + /// Path to the bundle generated for this module (if any). + var bundlePath: AbsolutePath? { + switch self { + case .swift(let buildDescription): + return buildDescription.bundlePath + case .clang(let buildDescription): + return buildDescription.bundlePath + } + } + + public var module: ResolvedModule { + switch self { + case .swift(let buildDescription): + return buildDescription.target + case .clang(let buildDescription): + return buildDescription.target + } + } + + public var package: ResolvedPackage { + switch self { + case .swift(let description): + description.package + case .clang(let description): + description.package + } + } + + /// Paths to the binary libraries the target depends on. + var libraryBinaryPaths: Set { + switch self { + case .swift(let target): + return target.libraryBinaryPaths + case .clang(let target): + return target.libraryBinaryPaths + } + } + + var resourceBundleInfoPlistPath: AbsolutePath? { + switch self { + case .swift(let buildDescription): + return buildDescription.resourceBundleInfoPlistPath + case .clang(let buildDescription): + return buildDescription.resourceBundleInfoPlistPath + } + } + + var buildToolPluginInvocationResults: [BuildToolPluginInvocationResult] { + switch self { + case .swift(let buildDescription): + return buildDescription.buildToolPluginInvocationResults + case .clang(let buildDescription): + return buildDescription.buildToolPluginInvocationResults + } + } + + public var buildParameters: BuildParameters { + switch self { + case .swift(let buildDescription): + return buildDescription.buildParameters + case .clang(let buildDescription): + return buildDescription.buildParameters + } + } + + var destination: BuildParameters.Destination { + switch self { + case .swift(let buildDescription): + buildDescription.destination + case .clang(let buildDescription): + buildDescription.destination + } + } + + var toolsVersion: ToolsVersion { + switch self { + case .swift(let buildDescription): + return buildDescription.toolsVersion + case .clang(let buildDescription): + return buildDescription.toolsVersion + } + } + + public var diagnosticFiles: [AbsolutePath] { + switch self { + case .swift(let buildDescription): + buildDescription.diagnosticFiles + case .clang(_): + [] + } + } + /// Determines the arguments needed to run `swift-symbolgraph-extract` for + /// this module. + public func symbolGraphExtractArguments() throws -> [String] { + switch self { + case .swift(let buildDescription): try buildDescription.symbolGraphExtractArguments() + case .clang(let buildDescription): try buildDescription.symbolGraphExtractArguments() + } + } +} + +extension ModuleBuildDescription: Identifiable { + public struct ID: Hashable { + let moduleID: ResolvedModule.ID + let destination: BuildParameters.Destination + } + + public var id: ID { + ID(moduleID: self.module.id, destination: self.destination) + } +} + +extension ModuleBuildDescription { + package enum Dependency { + /// Not all of the modules and products have build descriptions + case product(ResolvedProduct, ProductBuildDescription?) + case module(ResolvedModule, ModuleBuildDescription?) + } + + package func dependencies(using plan: BuildPlan) -> [Dependency] { + self.module + .dependencies(satisfying: self.buildParameters.buildEnvironment) + .map { + switch $0 { + case .product(let product, _): + let productDescription = plan.description(for: product, context: self.destination) + return .product(product, productDescription) + case .module(let module, _): + let moduleDescription = plan.description(for: module, context: self.destination) + return .module(module, moduleDescription) + } + } + } + + package func recursiveDependencies(using plan: BuildPlan) -> [Dependency] { + var dependencies: [Dependency] = [] + plan.traverseDependencies(of: self) { product, _, description in + dependencies.append(.product(product, description)) + } onModule: { module, _, description in + dependencies.append(.module(module, description)) + } + return dependencies + } + + package func recursiveLinkDependencies(using plan: BuildPlan) -> [Dependency] { + var dependencies: [Dependency] = [] + plan.traverseLinkDependencies(of: self) { product, _, description in + dependencies.append(.product(product, description)) + } onModule: { module, _, description in + dependencies.append(.module(module, description)) + } + return dependencies + } +} diff --git a/Sources/Build/BuildDescription/PluginDescription.swift b/Sources/Build/BuildDescription/PluginBuildDescription.swift similarity index 67% rename from Sources/Build/BuildDescription/PluginDescription.swift rename to Sources/Build/BuildDescription/PluginBuildDescription.swift index 03a8d62dc0b..d0051a67b9a 100644 --- a/Sources/Build/BuildDescription/PluginDescription.swift +++ b/Sources/Build/BuildDescription/PluginBuildDescription.swift @@ -16,48 +16,52 @@ import PackageModel import struct Basics.InternalError import protocol Basics.FileSystem -/// Description for a plugin target. This is treated a bit differently from the -/// regular kinds of targets, and is not included in the LLBuild description. -/// But because the package graph and build plan are not loaded for incremental +/// Description for a plugin module. This is treated a bit differently from the +/// regular kinds of modules, and is not included in the LLBuild description. +/// But because the modules graph and build plan are not loaded for incremental /// builds, this information is included in the BuildDescription, and the plugin -/// targets are compiled directly. -public final class PluginDescription: Codable { +/// modules are compiled directly. +public final class PluginBuildDescription: Codable { /// The identity of the package in which the plugin is defined. public let package: PackageIdentity - /// The name of the plugin target in that package (this is also the name of + /// The name of the plugin module in that package (this is also the name of /// the plugin). - public let targetName: String + public let moduleName: String + + /// The language-level module name. + public let moduleC99Name: String /// The names of any plugin products in that package that vend the plugin /// to other packages. public let productNames: [String] - /// The tools version of the package that declared the target. This affects + /// The tools version of the package that declared the module. This affects /// the API that is available in the PackagePlugin module. public let toolsVersion: ToolsVersion /// Swift source files that comprise the plugin. public let sources: Sources - /// Initialize a new plugin target description. The target is expected to be + /// Initialize a new plugin module description. The module is expected to be /// a `PluginTarget`. init( - target: ResolvedTarget, + module: ResolvedModule, products: [ResolvedProduct], package: ResolvedPackage, toolsVersion: ToolsVersion, testDiscoveryTarget: Bool = false, fileSystem: FileSystem ) throws { - guard target.underlying is PluginTarget else { - throw InternalError("underlying target type mismatch \(target)") + guard module.underlying is PluginModule else { + throw InternalError("underlying target type mismatch \(module)") } self.package = package.identity - self.targetName = target.name + self.moduleName = module.name + self.moduleC99Name = module.c99name self.productNames = products.map(\.name) self.toolsVersion = toolsVersion - self.sources = target.sources + self.sources = module.sources } } diff --git a/Sources/Build/BuildDescription/ProductBuildDescription.swift b/Sources/Build/BuildDescription/ProductBuildDescription.swift index 98e3b60cbc2..5c91a7c11d6 100644 --- a/Sources/Build/BuildDescription/ProductBuildDescription.swift +++ b/Sources/Build/BuildDescription/ProductBuildDescription.swift @@ -12,9 +12,13 @@ import Basics import PackageGraph + +@_spi(SwiftPMInternal) import PackageModel + import OrderedCollections import SPMBuildCore +import TSCUtility import struct TSCBasic.SortedArray @@ -34,6 +38,11 @@ public final class ProductBuildDescription: SPMBuildCore.ProductBuildDescription /// The build parameters. public let buildParameters: BuildParameters + /// The destination for while this product is built. + public var destination: BuildParameters.Destination { + self.buildParameters.destination + } + /// All object files to link into this product. /// // Computed during build planning. @@ -47,7 +56,7 @@ public final class ProductBuildDescription: SPMBuildCore.ProductBuildDescription var additionalFlags: [String] = [] /// The list of targets that are going to be linked statically in this product. - var staticTargets: [ResolvedTarget] = [] + var staticTargets: [ResolvedModule] = [] /// The list of Swift modules that should be passed to the linker. This is required for debugging to work. var swiftASTs: SortedArray = .init() @@ -60,7 +69,8 @@ public final class ProductBuildDescription: SPMBuildCore.ProductBuildDescription /// Path to the temporary directory for this product. var tempsPath: AbsolutePath { - self.buildParameters.buildPath.appending(component: self.product.name + ".product") + let suffix = buildParameters.suffix + return self.buildParameters.buildPath.appending(component: "\(self.product.name)\(suffix).product") } /// Path to the link filelist file. @@ -119,15 +129,6 @@ public final class ProductBuildDescription: SPMBuildCore.ProductBuildDescription return ["-Xlinker", "-dead_strip"] } else if triple.isWindows() { return ["-Xlinker", "/OPT:REF"] - } else if triple.arch == .wasm32 { - // FIXME: wasm-ld strips data segments referenced through __start/__stop symbols - // during GC, and it removes Swift metadata sections like swift5_protocols - // We should add support of SHF_GNU_RETAIN-like flag for __attribute__((retain)) - // to LLVM and wasm-ld - // This workaround is required for not only WASI but also all WebAssembly triples - // using wasm-ld (e.g. wasm32-unknown-unknown). So this branch is conditioned by - // arch == .wasm32 - return [] } else { return ["-Xlinker", "--gc-sections"] } @@ -166,14 +167,14 @@ public final class ProductBuildDescription: SPMBuildCore.ProductBuildDescription args += ["-L", self.buildParameters.buildPath.pathString] args += try ["-o", binaryPath.pathString] args += ["-module-name", self.product.name.spm_mangledToC99ExtendedIdentifier()] - args += self.dylibs.map { "-l" + $0.product.name } + args += self.dylibs.map { "-l" + $0.product.name + $0.buildParameters.suffix } // Add arguments needed for code coverage if it is enabled. if self.buildParameters.testingParameters.enableCodeCoverage { args += ["-profile-coverage-mapping", "-profile-generate"] } - let containsSwiftTargets = self.product.containsSwiftTargets + let containsSwiftTargets = self.product.containsSwiftModules let derivedProductType: ProductType switch self.product.type { @@ -189,6 +190,14 @@ public final class ProductBuildDescription: SPMBuildCore.ProductBuildDescription var isLinkingStaticStdlib = false let triple = self.buildParameters.triple + + // radar://112671586 supress unnecessary warnings + if triple.isMacOSX { + args += ["-Xlinker", "-no_warn_duplicate_libraries"] + } + // We may also need to turn off locally defined symbol imported on Windows + // args += ["-Xlinker", "/ignore:4217"] + switch derivedProductType { case .macro: throw InternalError("macro not supported") // should never be reached @@ -198,8 +207,8 @@ public final class ProductBuildDescription: SPMBuildCore.ProductBuildDescription // No arguments for static libraries. return [] case .test: - // Test products are bundle when using objectiveC, executable when using test entry point. - switch self.buildParameters.testingParameters.testProductStyle { + // Test products are bundle when using Objective-C, executable when using test entry point. + switch self.buildParameters.testProductStyle { case .loadableBundle: args += ["-Xlinker", "-bundle"] case .entryPointExecutable: @@ -235,8 +244,8 @@ public final class ProductBuildDescription: SPMBuildCore.ProductBuildDescription // we will instead have generated a source file containing the redirect. // Support for linking tests against executables is conditional on the tools // version of the package that defines the executable product. - let executableTarget = try product.executableTarget - if let target = executableTarget.underlying as? SwiftTarget, + let executableTarget = try product.executableModule + if let target = executableTarget.underlying as? SwiftModule, self.toolsVersion >= .v5_5, self.buildParameters.driverParameters.canRenameEntrypointFunctionName, target.supportsTestableExecutablesFeature @@ -261,18 +270,44 @@ public final class ProductBuildDescription: SPMBuildCore.ProductBuildDescription // Set rpath such that dynamic libraries are looked up // adjacent to the product, unless overridden. - if !self.buildParameters.linkingParameters.shouldDisableLocalRpath { - if triple.isLinux() { + if triple.os != .noneOS, !self.buildParameters.linkingParameters.shouldDisableLocalRpath { + switch triple.objectFormat { + case .elf: args += ["-Xlinker", "-rpath=$ORIGIN"] - } else if triple.isDarwin() { + case .macho: let rpath = self.product.type == .test ? "@loader_path/../../../" : "@loader_path" args += ["-Xlinker", "-rpath", "-Xlinker", rpath] + default: + break } } args += ["@\(self.linkFileListPath.pathString)"] - // Embed the swift stdlib library path inside tests and executables on Darwin. if containsSwiftTargets { + // Pass experimental features to link jobs in addition to compile jobs. Preserve ordering while eliminating + // duplicates with `OrderedSet`. + var experimentalFeatures = OrderedSet() + var strictMemorySafety = false + for target in self.product.modules { + let swiftSettings = target.underlying.buildSettingsDescription.filter { $0.tool == .swift } + for kind in swiftSettings.map(\.kind) { + if case let .enableExperimentalFeature(feature) = kind { + experimentalFeatures.append(feature) + } else if kind == .strictMemorySafety { + strictMemorySafety = true + } + } + } + + for feature in experimentalFeatures { + args += ["-enable-experimental-feature", feature] + } + + if strictMemorySafety { + args.append("-strict-memory-safety") + } + + // Embed the swift stdlib library path inside tests and executables on Darwin. let useStdlibRpath: Bool switch self.product.type { case .library(let type): @@ -283,12 +318,12 @@ public final class ProductBuildDescription: SPMBuildCore.ProductBuildDescription throw InternalError("unexpectedly asked to generate linker arguments for a plugin product") } - // When deploying to macOS prior to macOS 12, add an rpath to the - // back-deployed concurrency libraries. if useStdlibRpath, triple.isMacOSX { let macOSSupportedPlatform = self.package.getSupportedPlatform(for: .macOS, usingXCTest: product.isLinkingXCTest) if macOSSupportedPlatform.version.major < 12 { + // When deploying to macOS prior to macOS 12, add an rpath to the + // back-deployed concurrency libraries. let backDeployedStdlib = try buildParameters.toolchain.macosSwiftStdlib .parentDirectory .parentDirectory @@ -296,12 +331,25 @@ public final class ProductBuildDescription: SPMBuildCore.ProductBuildDescription .appending("macosx") args += ["-Xlinker", "-rpath", "-Xlinker", backDeployedStdlib.pathString] } - } - } - // Don't link runtime compatibility patch libraries if there are no - // Swift sources in the target. - if !containsSwiftTargets { + if macOSSupportedPlatform.version.major < 26 { + // When deploying to macOS prior to macOS 26, add an rpath to the + // back-deployed Span library. + let backDeployedStdlib = try buildParameters.toolchain.macosSwiftStdlib + .parentDirectory + .parentDirectory + .appending("swift-6.2") + .appending("macosx") + args += ["-Xlinker", "-rpath", "-Xlinker", backDeployedStdlib.pathString] + } + + // If either back deployment library is used, the driver is responsible for injecting + // a /usr/lib/swift -rpath at the front to ensure OS content is preferred when + // available. + } + } else { + // Don't link runtime compatibility patch libraries if there are no + // Swift sources in the target. args += ["-runtime-compatibility-version", "none"] } @@ -311,7 +359,7 @@ public final class ProductBuildDescription: SPMBuildCore.ProductBuildDescription // setting is the package-level right now. We might need to figure out a better // answer for libraries if/when we support specifying deployment target at the // target-level. - args += try self.buildParameters.targetTripleArgs(for: self.product.targets[0]) + args += try self.buildParameters.tripleArgs(for: self.product.modules[self.product.modules.startIndex]) // Add arguments from declared build settings. args += self.buildSettingsFlags @@ -346,7 +394,7 @@ public final class ProductBuildDescription: SPMBuildCore.ProductBuildDescription // Library search path for the toolchain's copy of SwiftSyntax. #if BUILD_MACROS_AS_DYLIBS if product.type == .macro { - args += try ["-L", buildParameters.toolchain.hostLibDir.pathString] + args += try ["-L", defaultBuildParameters.toolchain.hostLibDir.pathString] } #endif @@ -385,13 +433,24 @@ public final class ProductBuildDescription: SPMBuildCore.ProductBuildDescription } } +extension ProductBuildDescription: Identifiable { + public struct ID: Hashable { + let productID: ResolvedProduct.ID + let destination: BuildParameters.Destination + } + + public var id: ID { + ID(productID: self.product.id, destination: self.destination) + } +} + extension SortedArray where Element == AbsolutePath { public static func +=(lhs: inout SortedArray, rhs: S) where S.Iterator.Element == AbsolutePath { lhs.insert(contentsOf: rhs) } } -extension Triple { +extension Basics.Triple { var supportsFrameworks: Bool { return self.isDarwin() } diff --git a/Sources/Build/BuildDescription/ResolvedModule+BuildDescription.swift b/Sources/Build/BuildDescription/ResolvedModule+BuildDescription.swift new file mode 100644 index 00000000000..7701ea32e28 --- /dev/null +++ b/Sources/Build/BuildDescription/ResolvedModule+BuildDescription.swift @@ -0,0 +1,23 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2015-2023 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 struct Basics.AbsolutePath +import struct PackageGraph.ResolvedModule + +import SPMBuildCore + +extension ResolvedModule { + func tempsPath(_ buildParameters: BuildParameters) -> AbsolutePath { + let suffix = buildParameters.suffix + return buildParameters.buildPath.appending(component: "\(self.c99name)\(suffix).build") + } +} diff --git a/Sources/Build/BuildDescription/SharedTargetBuildDescription.swift b/Sources/Build/BuildDescription/SharedTargetBuildDescription.swift deleted file mode 100644 index a4c0e467676..00000000000 --- a/Sources/Build/BuildDescription/SharedTargetBuildDescription.swift +++ /dev/null @@ -1,67 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift open source project -// -// Copyright (c) 2023 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 Basics -import PackageGraph -import PackageLoading -import PackageModel -import SPMBuildCore - -import struct Basics.AbsolutePath - -/// Shared functionality between `ClangTargetBuildDescription` and `SwiftTargetBuildDescription` with the eventual hope of having a single type. -struct SharedTargetBuildDescription { - static func computePluginGeneratedFiles( - target: ResolvedTarget, - toolsVersion: ToolsVersion, - additionalFileRules: [FileRuleDescription], - buildParameters: BuildParameters, - buildToolPluginInvocationResults: [BuildToolPluginInvocationResult], - prebuildCommandResults: [PrebuildCommandResult], - observabilityScope: ObservabilityScope - ) -> (pluginDerivedSources: Sources, pluginDerivedResources: [Resource]) { - var pluginDerivedSources = Sources(paths: [], root: buildParameters.dataPath) - - // Add any derived files that were declared for any commands from plugin invocations. - var pluginDerivedFiles = [AbsolutePath]() - for command in buildToolPluginInvocationResults.reduce([], { $0 + $1.buildCommands }) { - for absPath in command.outputFiles { - pluginDerivedFiles.append(absPath) - } - } - - // Add any derived files that were discovered from output directories of prebuild commands. - for result in prebuildCommandResults { - for path in result.derivedFiles { - pluginDerivedFiles.append(path) - } - } - - // Let `TargetSourcesBuilder` compute the treatment of plugin generated files. - let (derivedSources, derivedResources) = TargetSourcesBuilder.computeContents( - for: pluginDerivedFiles, - toolsVersion: toolsVersion, - additionalFileRules: additionalFileRules, - defaultLocalization: target.defaultLocalization, - targetName: target.name, - targetPath: target.underlying.path, - observabilityScope: observabilityScope - ) - let pluginDerivedResources = derivedResources - derivedSources.forEach { absPath in - let relPath = absPath.relative(to: pluginDerivedSources.root) - pluginDerivedSources.relativePaths.append(relPath) - } - - return (pluginDerivedSources, pluginDerivedResources) - } -} diff --git a/Sources/Build/BuildDescription/SwiftTargetBuildDescription.swift b/Sources/Build/BuildDescription/SwiftModuleBuildDescription.swift similarity index 63% rename from Sources/Build/BuildDescription/SwiftTargetBuildDescription.swift rename to Sources/Build/BuildDescription/SwiftModuleBuildDescription.swift index 6f120aabf02..9a59bf3fff2 100644 --- a/Sources/Build/BuildDescription/SwiftTargetBuildDescription.swift +++ b/Sources/Build/BuildDescription/SwiftModuleBuildDescription.swift @@ -11,10 +11,16 @@ //===----------------------------------------------------------------------===// import Basics + import Foundation import PackageGraph import PackageLoading +import TSCUtility + +@_spi(SwiftPMInternal) import PackageModel + +@_spi(SwiftPMInternal) import SPMBuildCore #if USE_IMPL_ONLY_IMPORTS @@ -25,24 +31,35 @@ import DriverSupport import struct TSCBasic.ByteString -/// Target description for a Swift target. -public final class SwiftTargetBuildDescription { +@available(*, deprecated, renamed: "SwiftModuleBuildDescription") +public typealias SwiftTargetBuildDescription = SwiftModuleBuildDescription + +/// Build description for a Swift module. +public final class SwiftModuleBuildDescription { /// The package this target belongs to. public let package: ResolvedPackage /// The target described by this target. - public let target: ResolvedTarget + public let target: ResolvedModule - private let swiftTarget: SwiftTarget + private let swiftTarget: SwiftModule /// The tools version of the package that declared the target. This can /// can be used to conditionalize semantically significant changes in how /// a target is built. public let toolsVersion: ToolsVersion - /// The build parameters. + /// The build parameters for this target. let buildParameters: BuildParameters + /// The destination for while this module is built. + public var destination: BuildParameters.Destination { + self.buildParameters.destination + } + + /// The build parameters for the macro dependencies of this target. + let macroBuildParameters: BuildParameters + /// Path to the temporary directory for this target. let tempsPath: AbsolutePath @@ -60,9 +77,10 @@ public final class SwiftTargetBuildDescription { /// Path to the bundle generated for this module (if any). var bundlePath: AbsolutePath? { if let bundleName = target.underlying.potentialBundleName, needsResourceBundle { - return self.buildParameters.bundlePath(named: bundleName) + let suffix = self.buildParameters.suffix + return self.buildParameters.bundlePath(named: bundleName + suffix) } else { - return .none + return nil } } @@ -70,10 +88,13 @@ public final class SwiftTargetBuildDescription { return resources.filter { $0.rule != .embedInCode }.isEmpty == false } - private var needsResourceEmbedding: Bool { - return resources.filter { $0.rule == .embedInCode }.isEmpty == false + var resourceFilesToEmbed: [AbsolutePath] { + return resources.filter { $0.rule == .embedInCode }.map { $0.path } } + /// The path to Swift source file embedding resource contents if needed. + private(set) var resourcesEmbeddingSource: AbsolutePath? + /// The list of all source files in the target, including the derived ones. public var sources: [AbsolutePath] { self.target.sources.paths + self.derivedSources.paths + self.pluginDerivedSources.paths @@ -88,6 +109,16 @@ public final class SwiftTargetBuildDescription { self.target.underlying.resources + self.pluginDerivedResources } + /// The list of files in the target that were marked as ignored. + public var ignored: [AbsolutePath] { + self.target.underlying.ignored + } + + /// The list of other kinds of files in the target. + public var others: [AbsolutePath] { + self.target.underlying.others + } + /// The objects in this target, containing either machine code or bitcode /// depending on the build parameters used. public var objects: [AbsolutePath] { @@ -106,16 +137,17 @@ public final class SwiftTargetBuildDescription { } var modulesPath: AbsolutePath { - return self.buildParameters.buildPath.appending(component: "Modules") + let suffix = self.buildParameters.suffix + return self.buildParameters.buildPath.appending(component: "Modules\(suffix)") } /// The path to the swiftmodule file after compilation. public var moduleOutputPath: AbsolutePath { // note: needs to be public because of sourcekit-lsp // If we're an executable and we're not allowing test targets to link against us, we hide the module. let triple = buildParameters.triple - let allowLinkingAgainstExecutables = (triple.isDarwin() || triple.isLinux() || triple.isWindows()) && self.toolsVersion >= .v5_5 + let allowLinkingAgainstExecutables = [.coff, .macho, .elf].contains(triple.objectFormat) && self.toolsVersion >= .v5_5 let dirPath = (target.type == .executable && !allowLinkingAgainstExecutables) ? self.tempsPath : self.modulesPath - return dirPath.appending(component: self.target.c99name + ".swiftmodule") + return dirPath.appending(component: "\(self.target.c99name).swiftmodule") } /// The path to the wrapped swift module which is created using the modulewrap tool. This is required @@ -125,7 +157,7 @@ public final class SwiftTargetBuildDescription { self.tempsPath.appending(component: self.target.c99name + ".swiftmodule.o") } - /// The path to the swifinterface file after compilation. + /// The path to the swiftinterface file after compilation. var parseableModuleInterfaceOutputPath: AbsolutePath { self.modulesPath.appending(component: self.target.c99name + ".swiftinterface") } @@ -139,11 +171,6 @@ public final class SwiftTargetBuildDescription { /// Any addition flags to be added. These flags are expected to be computed during build planning. var additionalFlags: [String] = [] - /// The swift version for this target. - var swiftVersion: SwiftLanguageVersion { - self.swiftTarget.swiftVersion - } - /// Describes the purpose of a test target, including any special roles such as containing a list of discovered /// tests or serving as the manifest target which contains the main entry point. public enum TestTargetRole { @@ -171,6 +198,10 @@ public final class SwiftTargetBuildDescription { /// True if this module needs to be parsed as a library based on the target type and the configuration /// of the source code var needsToBeParsedAsLibrary: Bool { + if buildParameters.sanitizers.sanitizers.contains(.fuzzer) { + return true + } + switch self.target.type { case .library, .test: return true @@ -185,40 +216,12 @@ public final class SwiftTargetBuildDescription { return false } // looking into the file content to see if it is using the @main annotation which requires parse-as-library - return (try? self.containsAtMain(fileSystem: self.fileSystem, path: self.sources[0])) ?? false + return (try? containsAtMain(fileSystem: self.fileSystem, path: self.sources[0])) ?? false default: return false } } - // looking into the file content to see if it is using the @main annotation - // this is not bullet-proof since theoretically the file can contain the @main string for other reasons - // but it is the closest to accurate we can do at this point - func containsAtMain(fileSystem: FileSystem, path: AbsolutePath) throws -> Bool { - let content: String = try self.fileSystem.readFileContents(path) - let lines = content.split(separator: "\n").compactMap { String($0).spm_chuzzle() } - - var multilineComment = false - for line in lines { - if line.hasPrefix("//") { - continue - } - if line.hasPrefix("/*") { - multilineComment = true - } - if line.hasSuffix("*/") { - multilineComment = false - } - if multilineComment { - continue - } - if line.hasPrefix("@main") { - return true - } - } - return false - } - /// The filesystem to operate on. let fileSystem: FileSystem @@ -229,10 +232,15 @@ public final class SwiftTargetBuildDescription { public let buildToolPluginInvocationResults: [BuildToolPluginInvocationResult] /// The results of running any prebuild commands for this target. - public let prebuildCommandResults: [PrebuildCommandResult] + public let prebuildCommandResults: [CommandPluginResult] - /// Any macro products that this target requires to build. - public let requiredMacroProducts: [ResolvedProduct] + public var requiredMacros: [ResolvedModule] { + get throws { + try self.target.recursiveModuleDependencies().filter { + $0.type == .macro + } + } + } /// ObservabilityScope with which to emit diagnostics private let observabilityScope: ObservabilityScope @@ -241,25 +249,28 @@ public final class SwiftTargetBuildDescription { private let shouldGenerateTestObservation: Bool /// Whether to disable sandboxing (e.g. for macros). - private let disableSandbox: Bool + private let shouldDisableSandbox: Bool + + /// Whether to add -static on Windows to reduce symbol exports + public var isWindowsStatic: Bool /// Create a new target description with target and build parameters. init( package: ResolvedPackage, - target: ResolvedTarget, + target: ResolvedModule, toolsVersion: ToolsVersion, additionalFileRules: [FileRuleDescription] = [], buildParameters: BuildParameters, + macroBuildParameters: BuildParameters, buildToolPluginInvocationResults: [BuildToolPluginInvocationResult] = [], - prebuildCommandResults: [PrebuildCommandResult] = [], - requiredMacroProducts: [ResolvedProduct] = [], + prebuildCommandResults: [CommandPluginResult] = [], testTargetRole: TestTargetRole? = nil, shouldGenerateTestObservation: Bool = false, - disableSandbox: Bool, + shouldDisableSandbox: Bool, fileSystem: FileSystem, observabilityScope: ObservabilityScope ) throws { - guard let swiftTarget = target.underlying as? SwiftTarget else { + guard let swiftTarget = target.underlying as? SwiftModule else { throw InternalError("underlying target type mismatch \(target)") } @@ -268,6 +279,8 @@ public final class SwiftTargetBuildDescription { self.target = target self.toolsVersion = toolsVersion self.buildParameters = buildParameters + self.macroBuildParameters = macroBuildParameters + // Unless mentioned explicitly, use the target type to determine if this is a test target. if let testTargetRole { self.testTargetRole = testTargetRole @@ -277,26 +290,28 @@ public final class SwiftTargetBuildDescription { self.testTargetRole = nil } - self.tempsPath = buildParameters.buildPath.appending(component: target.c99name + ".build") + self.tempsPath = target.tempsPath(self.buildParameters) self.derivedSources = Sources(paths: [], root: self.tempsPath.appending("DerivedSources")) self.buildToolPluginInvocationResults = buildToolPluginInvocationResults self.prebuildCommandResults = prebuildCommandResults - self.requiredMacroProducts = requiredMacroProducts self.shouldGenerateTestObservation = shouldGenerateTestObservation - self.disableSandbox = disableSandbox + self.shouldDisableSandbox = shouldDisableSandbox self.fileSystem = fileSystem self.observabilityScope = observabilityScope - (self.pluginDerivedSources, self.pluginDerivedResources) = SharedTargetBuildDescription.computePluginGeneratedFiles( + (self.pluginDerivedSources, self.pluginDerivedResources) = ModulesGraph.computePluginGeneratedFiles( target: target, toolsVersion: toolsVersion, additionalFileRules: additionalFileRules, - buildParameters: buildParameters, + buildParameters: self.buildParameters, buildToolPluginInvocationResults: buildToolPluginInvocationResults, prebuildCommandResults: prebuildCommandResults, observabilityScope: observabilityScope ) + // default to -static on Windows + self.isWindowsStatic = buildParameters.triple.isWindows() + if self.shouldEmitObjCCompatibilityHeader { self.moduleMap = try self.generateModuleMap() } @@ -311,7 +326,10 @@ public final class SwiftTargetBuildDescription { } } - try self.generateResourceEmbeddingCode() + if !resourceFilesToEmbed.isEmpty { + resourcesEmbeddingSource = try addResourceEmbeddingSource() + } + try self.generateTestObservation() } @@ -328,7 +346,10 @@ public final class SwiftTargetBuildDescription { return } - guard self.buildParameters.triple.isDarwin(), self.buildParameters.testingParameters.experimentalTestOutput else { + guard + self.buildParameters.triple.isDarwin() && + self.buildParameters.testingParameters.experimentalTestOutput + else { return } @@ -339,31 +360,10 @@ public final class SwiftTargetBuildDescription { try self.fileSystem.writeIfChanged(path: path, string: content) } - // FIXME: This will not work well for large files, as we will store the entire contents, plus its byte array representation in memory and also `writeIfChanged()` will read the entire generated file again. - private func generateResourceEmbeddingCode() throws { - guard needsResourceEmbedding else { return } - - var content = - """ - struct PackageResources { - - """ - - try resources.forEach { - guard $0.rule == .embedInCode else { return } - - let variableName = $0.path.basename.spm_mangledToC99ExtendedIdentifier() - let fileContent = try Data(contentsOf: URL(fileURLWithPath: $0.path.pathString)).map { String($0) }.joined(separator: ",") - - content += "static let \(variableName): [UInt8] = [\(fileContent)]\n" - } - - content += "}" - + private func addResourceEmbeddingSource() throws -> AbsolutePath { let subpath = try RelativePath(validating: "embedded_resources.swift") self.derivedSources.relativePaths.append(subpath) - let path = self.derivedSources.root.appending(subpath) - try self.fileSystem.writeIfChanged(path: path, string: content) + return self.derivedSources.root.appending(subpath) } /// Generate the resource bundle accessor, if appropriate. @@ -419,55 +419,35 @@ public final class SwiftTargetBuildDescription { try self.fileSystem.writeIfChanged(path: path, string: content) } - private func packageNameArgumentIfSupported(with pkg: ResolvedPackage, packageAccess: Bool) -> [String] { - let flag = "-package-name" - if pkg.manifest.usePackageNameFlag, - DriverSupport.checkToolchainDriverFlags(flags: [flag], toolchain: self.buildParameters.toolchain, fileSystem: self.fileSystem) { - if packageAccess { - let pkgID = pkg.identity.description.spm_mangledToC99ExtendedIdentifier() - return [flag, pkgID] - } - } - return [] - } - private func macroArguments() throws -> [String] { var args = [String]() #if BUILD_MACROS_AS_DYLIBS - self.requiredMacroProducts.forEach { macro in - args += ["-Xfrontend", "-load-plugin-library", "-Xfrontend", self.buildParameters.binaryPath(for: macro).pathString] + try self.requiredMacros.forEach { macro in + args += [ + "-Xfrontend", "-load-plugin-library", + "-Xfrontend", macroBuildParameters.macroBinaryPath(macro).pathString + ] } #else - try self.requiredMacroProducts.forEach { macro in - if let macroTarget = macro.targets.first { - let executablePath = try self.buildParameters.binaryPath(for: macro).pathString - args += ["-Xfrontend", "-load-plugin-executable", "-Xfrontend", "\(executablePath)#\(macroTarget.c99name)"] - } else { - throw InternalError("macro product \(macro.name) has no targets") // earlier validation should normally catch this - } + let macroModules = try self.requiredMacros + try macroModules.forEach { macro in + let executablePath = try macroBuildParameters.macroBinaryPath(macro).pathString + args += ["-Xfrontend", "-load-plugin-executable", "-Xfrontend", "\(executablePath)#\(macro.c99name)"] } #endif - // If we're using an OSS toolchain, add the required arguments bringing in the plugin server from the default toolchain if available. - if self.buildParameters.toolchain.isSwiftDevelopmentToolchain, DriverSupport.checkSupportedFrontendFlags(flags: ["-external-plugin-path"], toolchain: self.buildParameters.toolchain, fileSystem: self.fileSystem), let pluginServer = try self.buildParameters.toolchain.swiftPluginServerPath { - let toolchainUsrPath = pluginServer.parentDirectory.parentDirectory - let pluginPathComponents = ["lib", "swift", "host", "plugins"] - - let pluginPath = toolchainUsrPath.appending(components: pluginPathComponents) - args += ["-Xfrontend", "-external-plugin-path", "-Xfrontend", "\(pluginPath)#\(pluginServer.pathString)"] - - let localPluginPath = toolchainUsrPath.appending(components: ["local"] + pluginPathComponents) - args += ["-Xfrontend", "-external-plugin-path", "-Xfrontend", "\(localPluginPath)#\(pluginServer.pathString)"] - } - - if self.disableSandbox { - let toolchainSupportsDisablingSandbox = DriverSupport.checkSupportedFrontendFlags(flags: ["-disable-sandbox"], toolchain: self.buildParameters.toolchain, fileSystem: fileSystem) + if self.shouldDisableSandbox { + let toolchainSupportsDisablingSandbox = DriverSupport.checkSupportedFrontendFlags( + flags: ["-disable-sandbox"], + toolchain: self.buildParameters.toolchain, + fileSystem: fileSystem + ) if toolchainSupportsDisablingSandbox { args += ["-disable-sandbox"] } else { // If there's at least one macro being used, we warn about our inability to disable sandboxing. - if !self.requiredMacroProducts.isEmpty { + if !macroModules.isEmpty { observabilityScope.emit(warning: "cannot disable sandboxing for Swift compilation because the selected toolchain does not support it") } } @@ -479,24 +459,24 @@ public final class SwiftTargetBuildDescription { /// The arguments needed to compile this target. public func compileArguments() throws -> [String] { var args = [String]() - args += try self.buildParameters.targetTripleArgs(for: self.target) - args += ["-swift-version", self.swiftVersion.rawValue] + args += try self.buildParameters.tripleArgs(for: self.target) // pass `-v` during verbose builds. if self.buildParameters.outputParameters.isVerbose { args += ["-v"] } - // Enable batch mode in debug mode. - // - // Technically, it should be enabled whenever WMO is off but we - // don't currently make that distinction in SwiftPM - switch self.buildParameters.configuration { - case .debug: - args += ["-enable-batch-mode"] - case .release: break + if self.useWholeModuleOptimization { + args.append("-whole-module-optimization") + args.append("-num-threads") + args.append(String(ProcessInfo.processInfo.activeProcessorCount)) + } else { + args.append("-incremental") + args.append("-enable-batch-mode") } + args += ["-serialize-diagnostics"] + args += self.buildParameters.indexStoreArguments(for: self.target) args += self.optimizationArguments args += self.testingArguments @@ -517,7 +497,7 @@ public final class SwiftTargetBuildDescription { // when we link the executable, we will ask the linker to rename the entry point // symbol to just `_main` again (or if the linker doesn't support it, we'll // generate a source containing a redirect). - if (self.target.underlying as? SwiftTarget)?.supportsTestableExecutablesFeature == true + if (self.target.underlying as? SwiftModule)?.supportsTestableExecutablesFeature == true && !self.isTestTarget && self.toolsVersion >= .v5_5 { // We only do this if the linker supports it, as indicated by whether we @@ -538,6 +518,11 @@ public final class SwiftTargetBuildDescription { args += ["-parse-as-library"] } + // Add -static to reduce symbol export count + if self.isWindowsStatic { + args += ["-static"] + } + // Only add the build path to the framework search path if there are binary frameworks to link against. if !self.libraryBinaryPaths.isEmpty { args += ["-F", self.buildParameters.buildPath.pathString] @@ -558,26 +543,8 @@ public final class SwiftTargetBuildDescription { args += ["-color-diagnostics"] } - // If this is a generated test discovery target, it might import a test - // target that is built with C++ interop enabled. In that case, the test - // discovery target must enable C++ interop as well - switch testTargetRole { - case .discovery: - for dependency in try self.target.recursiveTargetDependencies() { - let dependencyScope = self.buildParameters.createScope(for: dependency) - let dependencySwiftFlags = dependencyScope.evaluate(.OTHER_SWIFT_FLAGS) - if let interopModeFlag = dependencySwiftFlags.first(where: { $0.hasPrefix("-cxx-interoperability-mode=") }) { - args += [interopModeFlag] - if interopModeFlag != "-cxx-interoperability-mode=off" { - if let cxxStandard = self.package.manifest.cxxLanguageStandard { - args += ["-Xcc", "-std=\(cxxStandard)"] - } - } - break - } - } - default: break - } + args += try self.cxxInteroperabilityModeArguments( + propagateFromCurrentModuleOtherSwiftFlags: false) // Add arguments from declared build settings. args += try self.buildSettingsFlags() @@ -588,6 +555,24 @@ public final class SwiftTargetBuildDescription { args += ["-emit-module-interface-path", self.parseableModuleInterfaceOutputPath.pathString] } + switch self.buildParameters.prepareForIndexing { + case .off: + break + case .on: + args += ["-Xfrontend", "-experimental-lazy-typecheck",] + if !args.contains("-enable-testing") { + // enable-testing needs the non-exportable-decls + args += ["-Xfrontend", "-experimental-skip-non-exportable-decls"] + } + fallthrough + case .noLazy: + args += [ + "-Xfrontend", "-experimental-skip-all-function-bodies", + "-Xfrontend", "-experimental-allow-module-with-compiler-errors", + "-Xfrontend", "-empty-abi-descriptor" + ] + } + args += self.buildParameters.toolchain.extraFlags.swiftCompilerFlags // User arguments (from -Xswiftc) should follow generated arguments to allow user overrides args += self.buildParameters.flags.swiftCompilerFlags @@ -619,21 +604,45 @@ public final class SwiftTargetBuildDescription { // suppress warnings if the package is remote if self.package.isRemote { - args += ["-suppress-warnings"] - // suppress-warnings and warnings-as-errors are mutually exclusive - if let index = args.firstIndex(of: "-warnings-as-errors") { - args.remove(at: index) + // suppress-warnings and the other warning control flags are mutually exclusive + var removeNextArg = false + args = args.filter { arg in + if removeNextArg { + removeNextArg = false + return false + } + switch arg { + case "-warnings-as-errors", "-no-warnings-as-errors": + return false + case "-Wwarning", "-Werror": + removeNextArg = true + return false + default: + return true + } } + guard !removeNextArg else { + throw InternalError("Unexpected '-Wwarning' or '-Werror' at the end of args") + } + args += ["-suppress-warnings"] } // Pass `-user-module-version` for versioned packages that aren't pre-releases. - if let version = package.manifest.version, version.prereleaseIdentifiers.isEmpty, version.buildMetadataIdentifiers.isEmpty, toolsVersion >= .vNext { + if + let version = package.manifest.version, + version.prereleaseIdentifiers.isEmpty && + version.buildMetadataIdentifiers.isEmpty && + toolsVersion >= .v6_0 + { args += ["-user-module-version", version.description] } - args += self.packageNameArgumentIfSupported(with: self.package, packageAccess: self.target.packageAccess) + args += self.package.packageNameArgument( + target: self.target, + isPackageNameSupported: self.buildParameters.driverParameters.isPackageAccessModifierSupported + ) args += try self.macroArguments() - + // rdar://117578677 // Pass -fno-omit-frame-pointer to support backtraces // this can be removed once the backtracer uses DWARF instead of frame pointers @@ -648,15 +657,107 @@ public final class SwiftTargetBuildDescription { return args } - /// When `scanInvocation` argument is set to `true`, omit the side-effect producing arguments - /// such as emitting a module or supplementary outputs. - public func emitCommandLine(scanInvocation: Bool = false) throws -> [String] { + /// Determines the arguments needed to run `swift-symbolgraph-extract` for + /// this module. + package func symbolGraphExtractArguments() throws -> [String] { + var args = [String]() + + args += ["-module-name", self.target.c99name] + args += try self.buildParameters.tripleArgs(for: self.target) + args += ["-module-cache-path", try self.buildParameters.moduleCache.pathString] + + args += try self.cxxInteroperabilityModeArguments( + propagateFromCurrentModuleOtherSwiftFlags: true) + + args += self.buildParameters.toolchain.extraFlags.swiftCompilerFlags + + // Include search paths determined during planning + args += self.additionalFlags + // FIXME: only pass paths to the actual dependencies of the module + // Include search paths for swift module dependencies. + args += ["-I", self.modulesPath.pathString] + + // FIXME: Only include valid args + // This condition should instead only include args which are known to be + // compatible instead of filtering out specific unknown args. + // + // swift-symbolgraph-extract does not support parsing `-use-ld=lld` and + // will silently error failing the operation. + args = args.filter { !$0.starts(with: "-use-ld=") } + return args + } + + // FIXME: this function should operation on a strongly typed buildSetting + // Move logic from PackageBuilder here. + /// Determines the arguments needed for cxx interop for this module. + func cxxInteroperabilityModeArguments( + // FIXME: Remove argument + // This argument is added as a stop gap to support generating arguments + // for tools which currently don't leverage "OTHER_SWIFT_FLAGS". In the + // fullness of time this function should operate on a strongly typed + // "interopMode" property of SwiftTargetBuildDescription instead of + // digging through "OTHER_SWIFT_FLAGS" manually. + propagateFromCurrentModuleOtherSwiftFlags: Bool + ) throws -> [String] { + func cxxInteroperabilityModeAndStandard( + for module: ResolvedModule + ) -> [String]? { + let scope = self.buildParameters.createScope(for: module) + let flags = scope.evaluate(.OTHER_SWIFT_FLAGS) + let mode = flags.first { $0.hasPrefix("-cxx-interoperability-mode=") } + guard let mode else { return nil } + // FIXME: Use a stored self.cxxLanguageStandard property + // It definitely should _never_ reach back into the manifest + if let cxxStandard = self.package.manifest.cxxLanguageStandard { + return [mode, "-Xcc", "-std=\(cxxStandard)"] + } else { + return [mode] + } + } + + if propagateFromCurrentModuleOtherSwiftFlags { + // Look for cxx interop mode in the current module, if set exit early, + // the flag is already present. + if let args = cxxInteroperabilityModeAndStandard(for: self.target) { + return args + } + } + + // Implicitly propagate cxx interop flags for generated test targets. + // If the current module doesn't have cxx interop mode set, search + // through the module's dependencies looking for the a module that + // enables cxx interop and copy it's flag. + switch self.testTargetRole { + case .discovery, .entryPoint: + for module in try self.target.recursiveModuleDependencies() { + if let args = cxxInteroperabilityModeAndStandard(for: module) { + return args + } + } + default: break + } + return [] + } + + /// - Parameters: + /// - scanInvocation: When `true`, omit the side-effect producing arguments such as emitting a module or + /// supplementary outputs. + /// - writeOutputFileMap: When `false`, we assume that an output file map for this command line already exists at + /// the expected location on disk. This is intended for SourceKit-LSP to get build settings for a file without + /// writing out an output file map as a side effect. We expect that preparation of the module has already + /// created the output file map. + public func emitCommandLine(scanInvocation: Bool = false, writeOutputFileMap: Bool = true) throws -> [String] { var result: [String] = [] result.append(self.buildParameters.toolchain.swiftCompilerPath.pathString) result.append("-module-name") result.append(self.target.c99name) - result.append(contentsOf: packageNameArgumentIfSupported(with: self.package, packageAccess: self.target.packageAccess)) + result.append( + contentsOf: self.package.packageNameArgument( + target: self.target, + isPackageNameSupported: self.buildParameters.driverParameters.isPackageAccessModifierSupported + ) + ) if !scanInvocation { result.append("-emit-dependencies") @@ -666,16 +767,12 @@ public final class SwiftTargetBuildDescription { result.append(self.moduleOutputPath.pathString) result.append("-output-file-map") - // FIXME: Eliminate side effect. - result.append(try self.writeOutputFileMap().pathString) - } - - if self.buildParameters.useWholeModuleOptimization { - result.append("-whole-module-optimization") - result.append("-num-threads") - result.append(String(ProcessInfo.processInfo.activeProcessorCount)) - } else { - result.append("-incremental") + let outputFileMapPath = self.tempsPath.appending("output-file-map.json") + if writeOutputFileMap { + // FIXME: Eliminate side effect. + try self.writeOutputFileMap(to: outputFileMapPath) + } + result.append(outputFileMapPath.pathString) } result.append("-c") @@ -690,11 +787,10 @@ public final class SwiftTargetBuildDescription { /// Returns true if ObjC compatibility header should be emitted. private var shouldEmitObjCCompatibilityHeader: Bool { - self.buildParameters.triple.isDarwin() && self.target.type == .library + self.target.type == .library } - func writeOutputFileMap() throws -> AbsolutePath { - let path = self.tempsPath.appending("output-file-map.json") + func writeOutputFileMap(to path: AbsolutePath) throws { let masterDepsPath = self.tempsPath.appending("master.swiftdeps") var content = @@ -704,7 +800,7 @@ public final class SwiftTargetBuildDescription { """# - if self.buildParameters.useWholeModuleOptimization { + if self.useWholeModuleOptimization { let moduleName = self.target.c99name content += #""" @@ -745,6 +841,7 @@ public final class SwiftTargetBuildDescription { let sourceFileName = source.basenameWithoutExt let partialModulePath = self.tempsPath.appending(component: sourceFileName + "~partial.swiftmodule") let swiftDepsPath = self.tempsPath.appending(component: sourceFileName + ".swiftdeps") + let diagnosticsPath = self.diagnosticFile(sourceFile: source) content += #""" @@ -752,7 +849,7 @@ public final class SwiftTargetBuildDescription { """# - if !self.buildParameters.useWholeModuleOptimization { + if !self.useWholeModuleOptimization { let depsPath = self.tempsPath.appending(component: sourceFileName + ".d") content += #""" @@ -766,7 +863,8 @@ public final class SwiftTargetBuildDescription { #""" "\#(objectKey)": "\#(object._nativePathString(escaped: true))", "swiftmodule": "\#(partialModulePath._nativePathString(escaped: true))", - "swift-dependencies": "\#(swiftDepsPath._nativePathString(escaped: true))" + "swift-dependencies": "\#(swiftDepsPath._nativePathString(escaped: true))", + "diagnostics": "\#(diagnosticsPath._nativePathString(escaped: true))" }\#((idx + 1) < sources.count ? "," : "") """# @@ -774,19 +872,24 @@ public final class SwiftTargetBuildDescription { content += "}\n" - try self.fileSystem.writeFileContents(path, string: content) - return path + try fileSystem.createDirectory(path.parentDirectory, recursive: true) + try self.fileSystem.writeFileContents(path, bytes: .init(encodingAsUTF8: content), atomically: true) + } + + /// Directory for the the compatibility header and module map generated for this target. + /// The whole directory should be usable as a header search path. + private var compatibilityHeaderDirectory: AbsolutePath { + tempsPath.appending("include") } /// Generates the module map for the Swift target and returns its path. private func generateModuleMap() throws -> AbsolutePath { - let path = self.tempsPath.appending(component: moduleMapFilename) + let path = self.compatibilityHeaderDirectory.appending(component: moduleMapFilename) let bytes = ByteString( #""" module \#(self.target.c99name) { header "\#(self.objCompatibilityHeaderPath.pathString)" - requires objc } """#.utf8 @@ -805,7 +908,7 @@ public final class SwiftTargetBuildDescription { /// Returns the path to the ObjC compatibility header for this Swift target. var objCompatibilityHeaderPath: AbsolutePath { - self.tempsPath.appending("\(self.target.name)-Swift.h") + self.compatibilityHeaderDirectory.appending("\(self.target.name)-Swift.h") } /// Returns the build flags from the declared build settings. @@ -813,6 +916,9 @@ public final class SwiftTargetBuildDescription { let scope = self.buildParameters.createScope(for: self.target) var flags: [String] = [] + // A custom swift version. + flags += scope.evaluate(.SWIFT_VERSION).flatMap { ["-swift-version", $0] } + // Swift defines. let swiftDefines = scope.evaluate(.SWIFT_ACTIVE_COMPILATION_CONDITIONS) flags += swiftDefines.map { "-D" + $0 } @@ -837,8 +943,8 @@ public final class SwiftTargetBuildDescription { // Include path for the toolchain's copy of SwiftSyntax. #if BUILD_MACROS_AS_DYLIBS - if target.type == .macro { - flags += try ["-I", self.buildParameters.toolchain.hostLibDir.pathString] + if module.type == .macro { + flags += try ["-I", self.defaultBuildParameters.toolchain.hostLibDir.pathString] } #endif @@ -856,6 +962,12 @@ public final class SwiftTargetBuildDescription { break } + if bundlePath != nil { + compilationConditions += ["-DSWIFT_MODULE_RESOURCE_BUNDLE_AVAILABLE"] + } else { + compilationConditions += ["-DSWIFT_MODULE_RESOURCE_BUNDLE_UNAVAILABLE"] + } + return compilationConditions } @@ -874,8 +986,16 @@ public final class SwiftTargetBuildDescription { if self.isTestTarget { // test targets must be built with -enable-testing // since its required for test discovery (the non objective-c reflection kind) - return ["-enable-testing"] - } else if self.buildParameters.testingParameters.enableTestability { + var result = ["-enable-testing"] + + // Test targets need to enable cross-import overlays because Swift + // Testing cannot directly link to most other modules and needs to + // provide API that works with e.g. Foundation. (Developers can + // override this flag by passing -disable-cross-import-overlays.) + result += ["-Xfrontend", "-enable-cross-import-overlays"] + + return result + } else if self.buildParameters.enableTestability { return ["-enable-testing"] } else { return [] @@ -904,4 +1024,84 @@ public final class SwiftTargetBuildDescription { return arguments } + + package var isEmbeddedSwift: Bool { + // If the target explicitly declares that it should build with Embedded + // Swift, then true. + let buildSettings = self.target.underlying.buildSettingsDescription + let swiftSettings = buildSettings.swiftSettings.map(\.kind) + for case .enableExperimentalFeature("Embedded") in swiftSettings { + return true + } + + // Otherwise dig through flags looking for -enable-experimental-feature + // Embedded. This is needed to handle Embedded being set via: + // - unsafeFlags + // - swift build cli flags + // - toolset flags + let queryFlags = ["-enable-experimental-feature", "Embedded"] + + let toolchainFlags = self.buildParameters.toolchain.extraFlags.swiftCompilerFlags + if toolchainFlags.contains(queryFlags) { return true } + + let generalFlags = self.buildParameters.flags.swiftCompilerFlags + if generalFlags.contains(queryFlags) { return true } + + return false + } + + /// Whether to build Swift code with whole module optimization (WMO) + /// enabled. + package var useWholeModuleOptimization: Bool { + if self.isEmbeddedSwift { return true } + + switch self.buildParameters.configuration { + case .debug: + return false + case .release: + return true + } + } +} + +extension SwiftModuleBuildDescription { + package func dependencies( + using plan: BuildPlan + ) -> [ModuleBuildDescription.Dependency] { + ModuleBuildDescription.swift(self).dependencies(using: plan) + } + + package func recursiveLinkDependencies( + using plan: BuildPlan + ) -> [ModuleBuildDescription.Dependency] { + ModuleBuildDescription.swift(self).recursiveLinkDependencies(using: plan) + } + + package func recursiveDependencies( + using plan: BuildPlan + ) -> [ModuleBuildDescription.Dependency] { + ModuleBuildDescription.swift(self).recursiveDependencies(using: plan) + } +} + +extension SwiftModuleBuildDescription { + package var diagnosticFiles: [AbsolutePath] { + // WMO builds have a single frontend invocation and produce a single + // diagnostic file named after the module. + if self.useWholeModuleOptimization { + return [ + self.diagnosticFile(name: self.target.name) + ] + } + + return self.sources.map(self.diagnosticFile(sourceFile:)) + } + + private func diagnosticFile(name: String) -> AbsolutePath { + self.tempsPath.appending(component: "\(name).dia") + } + + private func diagnosticFile(sourceFile: AbsolutePath) -> AbsolutePath { + self.diagnosticFile(name: sourceFile.basenameWithoutExt) + } } diff --git a/Sources/Build/BuildDescription/TargetBuildDescription.swift b/Sources/Build/BuildDescription/TargetBuildDescription.swift deleted file mode 100644 index e1331ca2543..00000000000 --- a/Sources/Build/BuildDescription/TargetBuildDescription.swift +++ /dev/null @@ -1,108 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift open source project -// -// Copyright (c) 2015-2023 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 Basics -import struct PackageGraph.ResolvedTarget -import struct PackageModel.Resource -import struct SPMBuildCore.BuildToolPluginInvocationResult -import struct SPMBuildCore.BuildParameters - -public enum BuildDescriptionError: Swift.Error { - case requestedFileNotPartOfTarget(targetName: String, requestedFilePath: AbsolutePath) -} - -/// A target description which can either be for a Swift or Clang target. -public enum TargetBuildDescription { - /// Swift target description. - case swift(SwiftTargetBuildDescription) - - /// Clang target description. - case clang(ClangTargetBuildDescription) - - /// The objects in this target. - var objects: [AbsolutePath] { - get throws { - switch self { - case .swift(let target): - return try target.objects - case .clang(let target): - return try target.objects - } - } - } - - /// The resources in this target. - var resources: [Resource] { - switch self { - case .swift(let target): - return target.resources - case .clang(let target): - return target.resources - } - } - - /// Path to the bundle generated for this module (if any). - var bundlePath: AbsolutePath? { - switch self { - case .swift(let target): - return target.bundlePath - case .clang(let target): - return target.bundlePath - } - } - - var target: ResolvedTarget { - switch self { - case .swift(let target): - return target.target - case .clang(let target): - return target.target - } - } - - /// Paths to the binary libraries the target depends on. - var libraryBinaryPaths: Set { - switch self { - case .swift(let target): - return target.libraryBinaryPaths - case .clang(let target): - return target.libraryBinaryPaths - } - } - - var resourceBundleInfoPlistPath: AbsolutePath? { - switch self { - case .swift(let target): - return target.resourceBundleInfoPlistPath - case .clang(let target): - return target.resourceBundleInfoPlistPath - } - } - - var buildToolPluginInvocationResults: [BuildToolPluginInvocationResult] { - switch self { - case .swift(let target): - return target.buildToolPluginInvocationResults - case .clang(let target): - return target.buildToolPluginInvocationResults - } - } - - var buildParameters: BuildParameters { - switch self { - case .swift(let swiftTargetBuildDescription): - return swiftTargetBuildDescription.buildParameters - case .clang(let clangTargetBuildDescription): - return clangTargetBuildDescription.buildParameters - } - } -} diff --git a/Sources/Build/BuildManifest/LLBuildManifestBuilder+Clang.swift b/Sources/Build/BuildManifest/LLBuildManifestBuilder+Clang.swift index 5ac42266c5b..34476408fff 100644 --- a/Sources/Build/BuildManifest/LLBuildManifestBuilder+Clang.swift +++ b/Sources/Build/BuildManifest/LLBuildManifestBuilder+Clang.swift @@ -13,13 +13,15 @@ import struct LLBuildManifest.Node import struct Basics.AbsolutePath import struct Basics.InternalError -import struct PackageGraph.ResolvedTarget +import class Basics.ObservabilityScope +import struct PackageGraph.ResolvedModule import PackageModel +import SPMBuildCore extension LLBuildManifestBuilder { /// Create a llbuild target for a Clang target description. func createClangCompileCommand( - _ target: ClangTargetBuildDescription + _ target: ClangModuleBuildDescription ) throws { var inputs: [Node] = [] @@ -31,30 +33,33 @@ extension LLBuildManifestBuilder { inputs.append(resourcesNode) } - func addStaticTargetInputs(_ target: ResolvedTarget) { - if case .swift(let desc)? = self.plan.targetMap[target], target.type == .library { + func addStaticTargetInputs(_ description: ModuleBuildDescription?) { + if case .swift(let desc) = description, desc.target.type == .library { inputs.append(file: desc.moduleOutputPath) } } - for dependency in target.target.dependencies(satisfying: target.buildEnvironment) { + for dependency in target.dependencies(using: self.plan) { switch dependency { - case .target(let target, _): - addStaticTargetInputs(target) + case .module(_, let description): + addStaticTargetInputs(description) - case .product(let product, _): + case .product(let product, let productDescription): switch product.type { case .executable, .snippet, .library(.dynamic), .macro: - guard let planProduct = plan.productMap[product] else { - throw InternalError("unknown product \(product)") + guard let productDescription else { + throw InternalError("No build description for product: \(product)") } // Establish a dependency on binary of the product. - let binary = try planProduct.binaryPath - inputs.append(file: binary) + try inputs.append(file: productDescription.binaryPath) case .library(.automatic), .library(.static), .plugin: - for target in product.targets { - addStaticTargetInputs(target) + for module in product.modules { + let dependencyDescription = self.plan.description( + for: module, + context: product.type == .plugin ? .host : target.destination + ) + addStaticTargetInputs(dependencyDescription) } case .test: break @@ -89,16 +94,16 @@ extension LLBuildManifestBuilder { ) } - try addBuildToolPlugins(.clang(target)) + let additionalInputs = try addBuildToolPlugins(.clang(target)) // Create a phony node to represent the entire target. - let targetName = target.target.getLLBuildTargetName(config: target.buildParameters.buildConfig) + let targetName = target.llbuildTargetName let output: Node = .virtual(targetName) self.manifest.addNode(output, toTarget: targetName) self.manifest.addPhonyCmd( name: output.name, - inputs: objectFileNodes, + inputs: objectFileNodes + additionalInputs, outputs: [output] ) @@ -109,4 +114,15 @@ extension LLBuildManifestBuilder { self.addNode(output, toTarget: .test) } } + + /// Create a llbuild target for a Clang target preparation + func createClangPrepareCommand( + _ target: ClangModuleBuildDescription + ) throws { + // Create the node for the target so you can --target it. + // It is a no-op for index preparation. + let targetName = target.llbuildTargetName + let output: Node = .virtual(targetName) + self.manifest.addNode(output, toTarget: targetName) + } } diff --git a/Sources/Build/BuildManifest/LLBuildManifestBuilder+Product.swift b/Sources/Build/BuildManifest/LLBuildManifestBuilder+Product.swift index c6a8a72699a..33eb6f4558f 100644 --- a/Sources/Build/BuildManifest/LLBuildManifestBuilder+Product.swift +++ b/Sources/Build/BuildManifest/LLBuildManifestBuilder+Product.swift @@ -10,12 +10,18 @@ // //===----------------------------------------------------------------------===// +import PackageModel + import struct Basics.AbsolutePath +import struct Basics.InternalError import struct LLBuildManifest.Node +import struct SPMBuildCore.BuildParameters +import struct PackageGraph.ResolvedModule +import struct PackageGraph.ResolvedProduct extension LLBuildManifestBuilder { func createProductCommand(_ buildProduct: ProductBuildDescription) throws { - let cmdName = try buildProduct.product.getCommandName(config: buildProduct.buildParameters.buildConfig) + let cmdName = try buildProduct.commandName // Add dependency on Info.plist generation on Darwin platforms. let testInputs: [AbsolutePath] @@ -26,7 +32,7 @@ extension LLBuildManifestBuilder { testInputs = [testBundleInfoPlistPath] self.manifest.addWriteInfoPlistCommand( - principalClass: "\(buildProduct.product.targets[0].c99name).SwiftPMXCTestObserver", + principalClass: "\(buildProduct.product.modules[buildProduct.product.modules.startIndex].c99name).SwiftPMXCTestObserver", outputPath: testBundleInfoPlistPath ) } else { @@ -34,7 +40,7 @@ extension LLBuildManifestBuilder { } // Create a phony node to represent the entire target. - let targetName = try buildProduct.product.getLLBuildTargetName(config: buildProduct.buildParameters.buildConfig) + let targetName = try buildProduct.llbuildTargetName let output: Node = .virtual(targetName) let finalProductNode: Node @@ -85,7 +91,7 @@ extension LLBuildManifestBuilder { outputPath: plistPath ) - let cmdName = try buildProduct.product.getCommandName(config: buildProduct.buildParameters.buildConfig) + let cmdName = try buildProduct.commandName let codeSigningOutput = Node.virtual(targetName + "-CodeSigning") try self.manifest.addShellCmd( name: "\(cmdName)-entitlements", @@ -107,7 +113,7 @@ extension LLBuildManifestBuilder { outputs: [output] ) - if self.plan.graph.reachableProducts.contains(buildProduct.product) { + if self.plan.graph.reachableProducts.contains(id: buildProduct.product.id) { if buildProduct.product.type != .test { self.addNode(output, toTarget: .main) } @@ -120,3 +126,88 @@ extension LLBuildManifestBuilder { ) } } + +extension ProductBuildDescription { + package var llbuildTargetName: String { + get throws { + try self.product.getLLBuildTargetName(buildParameters: self.buildParameters) + } + } + + package var commandName: String { + get throws { + try "C.\(self.llbuildTargetName)\(self.buildParameters.suffix)" + } + } +} + +fileprivate func llbuildNameWithoutExtension( + for product: String, + buildParameters: BuildParameters +) -> String { + "\(product)-\(buildParameters.triple.tripleString)-\(buildParameters.buildConfig)\(buildParameters.suffix)" +} + +fileprivate func executableName( + for product: String, + buildParameters: BuildParameters +) -> String { + "\(llbuildNameWithoutExtension(for: product, buildParameters: buildParameters)).exe" +} + +fileprivate func dynamicLibraryName( + for product: String, + buildParameters: BuildParameters +) -> String { + "\(llbuildNameWithoutExtension(for: product, buildParameters: buildParameters)).dylib" +} + +fileprivate func staticLibraryName( + for product: String, + buildParameters: BuildParameters +) -> String { + "\(llbuildNameWithoutExtension(for: product, buildParameters: buildParameters)).a" +} + +fileprivate func testName( + for testProduct: String, + buildParameters: BuildParameters +) -> String { + "\(llbuildNameWithoutExtension(for: testProduct, buildParameters: buildParameters)).test" +} + +func getLLBuildTargetName( + macro: ResolvedModule, + buildParameters: BuildParameters +) -> String { + assert(macro.type == .macro) + #if BUILD_MACROS_AS_DYLIBS + return dynamicLibraryName(for: macro.name, buildParameters: buildParameters) + #else + return executableName(for: macro.name, buildParameters: buildParameters) + #endif +} + +extension ResolvedProduct { + public func getLLBuildTargetName(buildParameters: BuildParameters) throws -> String { + switch type { + case .library(.dynamic): + return dynamicLibraryName(for: self.name, buildParameters: buildParameters) + case .test: + return testName(for: self.name, buildParameters: buildParameters) + case .library(.static): + return staticLibraryName(for: self.name, buildParameters: buildParameters) + case .library(.automatic): + throw InternalError("automatic library not supported") + case .executable, .snippet: + return executableName(for: self.name, buildParameters: buildParameters) + case .macro: + guard let macroModule = self.modules.first else { + throw InternalError("macro product \(self.name) has no targets") + } + return Build.getLLBuildTargetName(macro: macroModule, buildParameters: buildParameters) + case .plugin: + throw InternalError("unexpectedly asked for the llbuild target name of a plugin product") + } + } +} diff --git a/Sources/Build/BuildManifest/LLBuildManifestBuilder+Resources.swift b/Sources/Build/BuildManifest/LLBuildManifestBuilder+Resources.swift index 599c7c435d5..1f185cb4ce8 100644 --- a/Sources/Build/BuildManifest/LLBuildManifestBuilder+Resources.swift +++ b/Sources/Build/BuildManifest/LLBuildManifestBuilder+Resources.swift @@ -13,12 +13,14 @@ import struct LLBuildManifest.Node import struct Basics.RelativePath +import PackageModel + extension LLBuildManifestBuilder { /// Adds command for creating the resources bundle of the given target. /// /// Returns the virtual node that will build the entire bundle. func createResourcesBundle( - for target: TargetBuildDescription + for target: ModuleBuildDescription ) throws -> Node? { guard let bundlePath = target.bundlePath else { return nil } @@ -45,7 +47,7 @@ extension LLBuildManifestBuilder { outputs.append(output) } - let cmdName = target.target.getLLBuildResourcesCmdName(config: target.buildParameters.buildConfig) + let cmdName = target.llbuildResourcesCmdName self.manifest.addPhonyCmd(name: cmdName, inputs: outputs, outputs: [.virtual(cmdName)]) return .virtual(cmdName) diff --git a/Sources/Build/BuildManifest/LLBuildManifestBuilder+Swift.swift b/Sources/Build/BuildManifest/LLBuildManifestBuilder+Swift.swift index 3a9ca3bd3bb..0616405e89b 100644 --- a/Sources/Build/BuildManifest/LLBuildManifestBuilder+Swift.swift +++ b/Sources/Build/BuildManifest/LLBuildManifestBuilder+Swift.swift @@ -17,31 +17,35 @@ import struct Basics.TSCAbsolutePath import struct LLBuildManifest.Node import struct LLBuildManifest.LLBuildManifest import struct SPMBuildCore.BuildParameters -import struct PackageGraph.ResolvedTarget +import struct PackageGraph.ResolvedModule import protocol TSCBasic.FileSystem -import enum TSCBasic.ProcessEnv import func TSCBasic.topologicalSort +import struct Basics.Environment #if USE_IMPL_ONLY_IMPORTS @_implementationOnly import class DriverSupport.SPMSwiftDriverExecutor +@_implementationOnly import Foundation @_implementationOnly import SwiftDriver +@_implementationOnly import TSCUtility #else import class DriverSupport.SPMSwiftDriverExecutor +import Foundation import SwiftDriver +import TSCUtility #endif import PackageModel extension LLBuildManifestBuilder { - /// Create a llbuild target for a Swift target description. + /// Create a llbuild target for a Swift module description. func createSwiftCompileCommand( - _ target: SwiftTargetBuildDescription + _ target: SwiftModuleBuildDescription ) throws { // Inputs. let inputs = try self.computeSwiftCompileCmdInputs(target) // Outputs. - let objectNodes = try target.objects.map(Node.file) + let objectNodes = target.buildParameters.prepareForIndexing == .off ? try target.objects.map(Node.file) : [] let moduleNode = Node.file(target.moduleOutputPath) let cmdOutputs = objectNodes + [moduleNode] @@ -60,12 +64,12 @@ extension LLBuildManifestBuilder { } private func addSwiftCmdsViaIntegratedDriver( - _ target: SwiftTargetBuildDescription, + _ target: SwiftModuleBuildDescription, inputs: [Node], moduleNode: Node ) throws { // Use the integrated Swift driver to compute the set of frontend - // jobs needed to build this Swift target. + // jobs needed to build this Swift module. var commandLine = try target.emitCommandLine() commandLine.append("-driver-use-frontend-path") commandLine.append(target.buildParameters.toolchain.swiftCompilerPath.pathString) @@ -75,13 +79,14 @@ extension LLBuildManifestBuilder { let executor = SPMSwiftDriverExecutor( resolver: resolver, fileSystem: target.fileSystem, - env: ProcessEnv.vars + env: Environment.current ) var driver = try Driver( args: commandLine, diagnosticsOutput: .handler(self.observabilityScope.makeDiagnosticsHandler()), fileSystem: self.fileSystem, - executor: executor + executor: executor, + compilerIntegratedTooling: false ) try driver.checkLDPathOption(commandLine: commandLine) @@ -96,12 +101,11 @@ extension LLBuildManifestBuilder { } private func addSwiftDriverJobs( - for targetDescription: SwiftTargetBuildDescription, + for targetDescription: SwiftModuleBuildDescription, jobs: [Job], inputs: [Node], resolver: ArgsResolver, isMainModule: (Job) -> Bool, - uniqueExplicitDependencyTracker: UniqueExplicitDependencyJobTracker? = nil ) throws { // Add build jobs to the manifest for job in jobs { @@ -109,30 +113,18 @@ extension LLBuildManifestBuilder { let commandLine = try job.commandLine.map { try resolver.resolve($0) } let arguments = [tool] + commandLine - // Check if an explicit pre-build dependency job has already been - // added as a part of this build. - if let uniqueExplicitDependencyTracker, - job.isExplicitDependencyPreBuildJob - { - if try !uniqueExplicitDependencyTracker.registerExplicitDependencyBuildJob(job) { - // This is a duplicate of a previously-seen identical job. - // Skip adding it to the manifest - continue - } - } - let jobInputs = try job.inputs.map { try $0.resolveToNode(fileSystem: self.fileSystem) } let jobOutputs = try job.outputs.map { try $0.resolveToNode(fileSystem: self.fileSystem) } - // Add target dependencies as inputs to the main module build command. + // Add module dependencies as inputs to the main module build command. // - // Jobs for a target's intermediate build artifacts, such as PCMs or + // Jobs for a module's intermediate build artifacts, such as PCMs or // modules built from a .swiftinterface, do not have a - // dependency on cross-target build products. If multiple targets share + // dependency on cross-module build products. If multiple targets share // common intermediate dependency modules, such dependencies can lead // to cycles in the resulting manifest. var manifestNodeInputs: [Node] = [] - if targetDescription.buildParameters.driverParameters.useExplicitModuleBuild && !isMainModule(job) { + if !isMainModule(job) { manifestNodeInputs = jobInputs } else { manifestNodeInputs = (inputs + jobInputs).uniqued() @@ -167,217 +159,18 @@ extension LLBuildManifestBuilder { } } - // Building a Swift module in Explicit Module Build mode requires passing all of its module - // dependencies as explicit arguments to the build command. Thus, building a SwiftPM package - // with multiple inter-dependent targets requires that each target’s build job must - // have its target dependencies’ modules passed into it as explicit module dependencies. - // Because none of the targets have been built yet, a given target's dependency scanning - // action will not be able to discover its target dependencies' modules. Instead, it is - // SwiftPM's responsibility to communicate to the driver, when planning a given target's - // build, that this target has dependencies that are other targets, along with a list of - // future artifacts of such dependencies (.swiftmodule and .pcm files). - // The driver will then use those artifacts as explicit inputs to its module’s build jobs. - // - // Consider an example SwiftPM package with two targets: target B, and target A, where A - // depends on B: - // SwiftPM will process targets in a topological order and “bubble-up” each target’s - // inter-module dependency graph to its dependencies. First, SwiftPM will process B, and be - // able to plan its full build because it does not have any target dependencies. Then the - // driver is tasked with planning a build for A. SwiftPM will pass as input to the driver - // the module dependency graph of its target’s dependencies, in this case, just the - // dependency graph of B. The driver is then responsible for the necessary post-processing - // to merge the dependency graphs and plan the build for A, using artifacts of B as explicit - // inputs. - public func addTargetsToExplicitBuildManifest() throws { - // Sort the product targets in topological order in order to collect and "bubble up" - // their respective dependency graphs to the depending targets. - let nodes: [ResolvedTarget.Dependency] = self.plan.targetMap.keys.map { - ResolvedTarget.Dependency.target($0, conditions: []) - } - let allPackageDependencies = try topologicalSort(nodes, successors: { $0.dependencies }) - // Instantiate the inter-module dependency oracle which will cache commonly-scanned - // modules across targets' Driver instances. - let dependencyOracle = InterModuleDependencyOracle() - - // Explicit dependency pre-build jobs may be common to multiple targets. - // We de-duplicate them here to avoid adding identical entries to the - // downstream LLBuild manifest - let explicitDependencyJobTracker = UniqueExplicitDependencyJobTracker() - - // Create commands for all target descriptions in the plan. - for dependency in allPackageDependencies.reversed() { - guard case .target(let target, _) = dependency else { - // Product dependency build jobs are added after the fact. - // Targets that depend on product dependencies will expand the corresponding - // product into its constituent targets. - continue - } - guard target.underlying.type != .systemModule, - target.underlying.type != .binary - else { - // Much like non-Swift targets, system modules will consist of a modulemap - // somewhere in the filesystem, with the path to that module being either - // manually-specified or computed based on the system module type (apt, brew). - // Similarly, binary targets will bring in an .xcframework, the contents of - // which will be exposed via search paths. - // - // In both cases, the dependency scanning action in the driver will be automatically - // be able to detect such targets' modules. - continue - } - guard let description = plan.targetMap[target] else { - throw InternalError("Expected description for target \(target)") - } - switch description { - case .swift(let desc): - try self.createExplicitSwiftTargetCompileCommand( - description: desc, - dependencyOracle: dependencyOracle, - explicitDependencyJobTracker: explicitDependencyJobTracker - ) - case .clang(let desc): - try self.createClangCompileCommand(desc) - } - } - } - - private func createExplicitSwiftTargetCompileCommand( - description: SwiftTargetBuildDescription, - dependencyOracle: InterModuleDependencyOracle, - explicitDependencyJobTracker: UniqueExplicitDependencyJobTracker? - ) throws { - // Inputs. - let inputs = try self.computeSwiftCompileCmdInputs(description) - - // Outputs. - let objectNodes = try description.objects.map(Node.file) - let moduleNode = Node.file(description.moduleOutputPath) - let cmdOutputs = objectNodes + [moduleNode] - - // Commands. - try addExplicitBuildSwiftCmds( - description, - inputs: inputs, - dependencyOracle: dependencyOracle, - explicitDependencyJobTracker: explicitDependencyJobTracker - ) - - self.addTargetCmd(description, cmdOutputs: cmdOutputs) - try self.addModuleWrapCmd(description) - } - - private func addExplicitBuildSwiftCmds( - _ targetDescription: SwiftTargetBuildDescription, - inputs: [Node], - dependencyOracle: InterModuleDependencyOracle, - explicitDependencyJobTracker: UniqueExplicitDependencyJobTracker? = nil - ) throws { - // Pass the driver its external dependencies (target dependencies) - var dependencyModuleDetailsMap: SwiftDriver.ExternalTargetModuleDetailsMap = [:] - // Collect paths for target dependencies of this target (direct and transitive) - try self.collectTargetDependencyModuleDetails( - for: .swift(targetDescription), - dependencyModuleDetailsMap: &dependencyModuleDetailsMap - ) - - // Compute the set of frontend - // jobs needed to build this Swift target. - var commandLine = try targetDescription.emitCommandLine() - commandLine.append("-driver-use-frontend-path") - commandLine.append(targetDescription.buildParameters.toolchain.swiftCompilerPath.pathString) - commandLine.append("-experimental-explicit-module-build") - let resolver = try ArgsResolver(fileSystem: self.fileSystem) - let executor = SPMSwiftDriverExecutor( - resolver: resolver, - fileSystem: self.fileSystem, - env: ProcessEnv.vars - ) - var driver = try Driver( - args: commandLine, - fileSystem: self.fileSystem, - executor: executor, - externalTargetModuleDetailsMap: dependencyModuleDetailsMap, - interModuleDependencyOracle: dependencyOracle - ) - try driver.checkLDPathOption(commandLine: commandLine) - - let jobs = try driver.planBuild() - try self.addSwiftDriverJobs( - for: targetDescription, - jobs: jobs, - inputs: inputs, - resolver: resolver, - isMainModule: { driver.isExplicitMainModuleJob(job: $0) }, - uniqueExplicitDependencyTracker: explicitDependencyJobTracker - ) - } - - /// Collect a map from all target dependencies of the specified target to the build planning artifacts for said - /// dependency, - /// in the form of a path to a .swiftmodule file and the dependency's InterModuleDependencyGraph. - private func collectTargetDependencyModuleDetails( - for targetDescription: TargetBuildDescription, - dependencyModuleDetailsMap: inout SwiftDriver.ExternalTargetModuleDetailsMap - ) throws { - for dependency in targetDescription.target.dependencies(satisfying: targetDescription.buildParameters.buildEnvironment) { - switch dependency { - case .product: - // Product dependencies are broken down into the targets that make them up. - guard let dependencyProduct = dependency.product else { - throw InternalError("unknown dependency product for \(dependency)") - } - for dependencyProductTarget in dependencyProduct.targets { - guard let dependencyTargetDescription = self.plan.targetMap[dependencyProductTarget] else { - throw InternalError("unknown dependency target for \(dependencyProductTarget)") - } - try self.addTargetDependencyInfo( - for: dependencyTargetDescription, - dependencyModuleDetailsMap: &dependencyModuleDetailsMap - ) - } - case .target: - // Product dependencies are broken down into the targets that make them up. - guard - let dependencyTarget = dependency.target, - let dependencyTargetDescription = self.plan.targetMap[dependencyTarget] - else { - throw InternalError("unknown dependency target for \(dependency)") - } - try self.addTargetDependencyInfo( - for: dependencyTargetDescription, - dependencyModuleDetailsMap: &dependencyModuleDetailsMap - ) - } - } - } - - private func addTargetDependencyInfo( - for targetDescription: TargetBuildDescription, - dependencyModuleDetailsMap: inout SwiftDriver.ExternalTargetModuleDetailsMap - ) throws { - guard case .swift(let dependencySwiftTargetDescription) = targetDescription else { - return - } - dependencyModuleDetailsMap[ModuleDependencyId.swiftPlaceholder(targetDescription.target.c99name)] = - SwiftDriver.ExternalTargetModuleDetails( - path: TSCAbsolutePath(dependencySwiftTargetDescription.moduleOutputPath), - isFramework: false - ) - try self.collectTargetDependencyModuleDetails( - for: targetDescription, - dependencyModuleDetailsMap: &dependencyModuleDetailsMap - ) - } - private func addCmdWithBuiltinSwiftTool( - _ target: SwiftTargetBuildDescription, + _ target: SwiftModuleBuildDescription, inputs: [Node], cmdOutputs: [Node] ) throws { let isLibrary = target.target.type == .library || target.target.type == .test - let cmdName = target.target.getCommandName(config: target.buildParameters.buildConfig) + let cmdName = target.getCommandName() self.manifest.addWriteSourcesFileListCommand(sources: target.sources, sourcesFileListPath: target.sourcesFileListPath) + let outputFileMapPath = target.tempsPath.appending("output-file-map.json") + // FIXME: Eliminate side effect. + try target.writeOutputFileMap(to: outputFileMapPath) self.manifest.addSwiftCmd( name: cmdName, inputs: inputs + [Node.file(target.sourcesFileListPath)], @@ -393,20 +186,21 @@ extension LLBuildManifestBuilder { sources: target.sources, fileList: target.sourcesFileListPath, isLibrary: isLibrary, - wholeModuleOptimization: target.buildParameters.configuration == .release, - outputFileMapPath: try target.writeOutputFileMap() // FIXME: Eliminate side effect. + wholeModuleOptimization: target.useWholeModuleOptimization, + outputFileMapPath: outputFileMapPath, + prepareForIndexing: target.buildParameters.prepareForIndexing != .off ) } private func computeSwiftCompileCmdInputs( - _ target: SwiftTargetBuildDescription + _ target: SwiftModuleBuildDescription ) throws -> [Node] { var inputs = target.sources.map(Node.file) let swiftVersionFilePath = addSwiftGetVersionCommand(buildParameters: target.buildParameters) inputs.append(.file(swiftVersionFilePath)) - // Add resources node as the input to the target. This isn't great because we + // Add resources node as the input to the module. This isn't great because we // don't need to block building of a module until its resources are assembled but // we don't currently have a good way to express that resources should be built // whenever a module is being built. @@ -414,59 +208,80 @@ extension LLBuildManifestBuilder { inputs.append(resourcesNode) } - func addStaticTargetInputs(_ target: ResolvedTarget) throws { + if let resourcesEmbeddingSource = target.resourcesEmbeddingSource { + let resourceFilesToEmbed = target.resourceFilesToEmbed + self.manifest.addWriteEmbeddedResourcesCommand(resources: resourceFilesToEmbed, outputPath: resourcesEmbeddingSource) + } + + let prepareForIndexing = target.buildParameters.prepareForIndexing + + func addStaticTargetInputs(_ module: ResolvedModule, _ description: ModuleBuildDescription?) throws { // Ignore C Modules. - if target.underlying is SystemLibraryTarget { return } + if module.underlying is SystemLibraryModule { return } // Ignore Binary Modules. - if target.underlying is BinaryTarget { return } - // Ignore Plugin Targets. - if target.underlying is PluginTarget { return } + if module.underlying is BinaryModule { return } + // Ignore Plugin Modules. + if module.underlying is PluginModule { return } + + guard let description else { + throw InternalError("No build description for module: \(module)") + } // Depend on the binary for executable targets. - if target.type == .executable { - // FIXME: Optimize. - let product = try plan.graph.allProducts.first { - try $0.type == .executable && $0.executableTarget == target - } - if let product { - guard let planProduct = plan.productMap[product] else { - throw InternalError("unknown product \(product)") - } - try inputs.append(file: planProduct.binaryPath) + if module.type == .executable && prepareForIndexing == .off { + // FIXME: Optimize. Build plan could build a mapping between executable modules + // and their products to speed up search here, which is inefficient if the plan + // contains a lot of products. + if let productDescription = try plan.productMap.values.first(where: { + try $0.product.type == .executable && + $0.product.executableModule.id == module.id && + $0.destination == description.destination + }) { + try inputs.append(file: productDescription.binaryPath) } return } - switch self.plan.targetMap[target] { - case .swift(let target)?: - inputs.append(file: target.moduleOutputPath) - case .clang(let target)?: - for object in try target.objects { - inputs.append(file: object) + switch description { + case .swift(let swiftDescription): + inputs.append(file: swiftDescription.moduleOutputPath) + case .clang(let clangDescription): + if prepareForIndexing != .off { + // In preparation, we're only building swiftmodules + // propagate the dependency to the header files in this target + for header in clangDescription.clangTarget.headers { + inputs.append(file: header) + } + } else { + for object in try clangDescription.objects { + inputs.append(file: object) + } } - case nil: - throw InternalError("unexpected: target \(target) not in target map \(self.plan.targetMap)") } } - for dependency in target.target.dependencies(satisfying: target.buildParameters.buildEnvironment) { + for dependency in target.dependencies(using: self.plan) { switch dependency { - case .target(let target, _): - try addStaticTargetInputs(target) + case .module(let module, let description): + try addStaticTargetInputs(module, description) - case .product(let product, _): + case .product(let product, let productDescription): switch product.type { case .executable, .snippet, .library(.dynamic), .macro: - guard let planProduct = plan.productMap[product] else { - throw InternalError("unknown product \(product)") + guard let productDescription else { + throw InternalError("No description for product: \(product)") } // Establish a dependency on binary of the product. - try inputs.append(file: planProduct.binaryPath) + try inputs.append(file: productDescription.binaryPath) // For automatic and static libraries, and plugins, add their targets as static input. case .library(.automatic), .library(.static), .plugin: - for target in product.targets { - try addStaticTargetInputs(target) + for module in product.modules { + let description = self.plan.description( + for: module, + context: product.type == .plugin ? .host : target.destination + ) + try addStaticTargetInputs(module, description) } case .test: @@ -484,20 +299,23 @@ extension LLBuildManifestBuilder { } } - try self.addBuildToolPlugins(.swift(target)) + let additionalInputs = try self.addBuildToolPlugins(.swift(target)) - // Depend on any required macro product's output. - try target.requiredMacroProducts.forEach { macro in - try inputs.append(.virtual(macro.getLLBuildTargetName(config: target.buildParameters.buildConfig))) + // Depend on any required macro's output. + try target.requiredMacros.forEach { macro in + inputs.append(.virtual(getLLBuildTargetName( + macro: macro, + buildParameters: target.macroBuildParameters + ))) } - return inputs + return inputs + additionalInputs } - /// Adds a top-level phony command that builds the entire target. - private func addTargetCmd(_ target: SwiftTargetBuildDescription, cmdOutputs: [Node]) { - // Create a phony node to represent the entire target. - let targetName = target.target.getLLBuildTargetName(config: target.buildParameters.buildConfig) + /// Adds a top-level phony command that builds the entire module. + private func addTargetCmd(_ target: SwiftModuleBuildDescription, cmdOutputs: [Node]) { + // Create a phony node to represent the entire module. + let targetName = target.getLLBuildTargetName() let targetOutput: Node = .virtual(targetName) self.manifest.addNode(targetOutput, toTarget: targetName) @@ -514,7 +332,7 @@ extension LLBuildManifestBuilder { } } - private func addModuleWrapCmd(_ target: SwiftTargetBuildDescription) throws { + private func addModuleWrapCmd(_ target: SwiftModuleBuildDescription) throws { // Add commands to perform the module wrapping Swift modules when debugging strategy is `modulewrap`. guard target.buildParameters.debuggingStrategy == .modulewrap else { return } var moduleWrapArgs = [ @@ -522,7 +340,7 @@ extension LLBuildManifestBuilder { "-modulewrap", target.moduleOutputPath.pathString, "-o", target.wrappedModuleOutputPath.pathString, ] - moduleWrapArgs += try target.buildParameters.targetTripleArgs(for: target.target) + moduleWrapArgs += try target.buildParameters.tripleArgs(for: target.target) self.manifest.addShellCmd( name: target.wrappedModuleOutputPath.pathString, description: "Wrapping AST for \(target.target.name) for debugging", @@ -549,32 +367,6 @@ extension LLBuildManifestBuilder { } } -extension SwiftDriver.Job { - fileprivate var isExplicitDependencyPreBuildJob: Bool { - (kind == .emitModule && inputs.contains { $0.file.extension == "swiftinterface" }) || kind == .generatePCM - } -} - -/// A simple mechanism to keep track of already-known explicit module pre-build jobs. -/// It uses the output filename of the job (either a `.swiftmodule` or a `.pcm`) for uniqueness, -/// because the SwiftDriver encodes the module's context hash into this filename. Any two jobs -/// producing an binary module file with an identical name are therefore duplicate -private class UniqueExplicitDependencyJobTracker { - private var uniqueDependencyModuleIDSet: Set = [] - - /// Registers the input Job with the tracker. Returns `false` if this job is already known - func registerExplicitDependencyBuildJob(_ job: SwiftDriver.Job) throws -> Bool { - guard job.isExplicitDependencyPreBuildJob, - let soleOutput = job.outputs.spm_only - else { - throw InternalError("Expected explicit module dependency build job") - } - let jobUniqueID = soleOutput.file.basename.hashValue - let (new, _) = self.uniqueDependencyModuleIDSet.insert(jobUniqueID) - return new - } -} - extension TypedVirtualPath { /// Resolve a typed virtual path provided by the Swift driver to /// a node in the build graph. @@ -603,3 +395,13 @@ extension Driver { } } } + +extension SwiftModuleBuildDescription { + public func getCommandName() -> String { + "C." + self.getLLBuildTargetName() + } + + public func getLLBuildTargetName() -> String { + self.target.getLLBuildTargetName(buildParameters: self.buildParameters) + } +} diff --git a/Sources/Build/BuildManifest/LLBuildManifestBuilder.swift b/Sources/Build/BuildManifest/LLBuildManifestBuilder.swift index 373fdc1d828..a8d7ef2126e 100644 --- a/Sources/Build/BuildManifest/LLBuildManifestBuilder.swift +++ b/Sources/Build/BuildManifest/LLBuildManifestBuilder.swift @@ -23,7 +23,6 @@ import SwiftDriver #endif import struct TSCBasic.ByteString -import enum TSCBasic.ProcessEnv import func TSCBasic.topologicalSort /// High-level interface to ``LLBuildManifest`` and ``LLBuildManifestWriter``. @@ -94,33 +93,32 @@ public class LLBuildManifestBuilder { addPackageStructureCommand() addBinaryDependencyCommands() - if self.plan.destinationBuildParameters.driverParameters.useExplicitModuleBuild { - // Explicit module builds use the integrated driver directly and - // require that every target's build jobs specify its dependencies explicitly to plan - // its build. - // Currently behind: - // --experimental-explicit-module-build - try addTargetsToExplicitBuildManifest() - } else { - // Create commands for all target descriptions in the plan. - for (_, description) in self.plan.targetMap { - switch description { - case .swift(let desc): - try self.createSwiftCompileCommand(desc) - case .clang(let desc): + // Create commands for all target descriptions in the plan. + for description in self.plan.targetMap { + switch description { + case .swift(let desc): + try self.createSwiftCompileCommand(desc) + case .clang(let desc): + if desc.buildParameters.prepareForIndexing == .off { try self.createClangCompileCommand(desc) + } else { + // Hook up the clang module target when preparing + try self.createClangPrepareCommand(desc) } } } - if self.plan.destinationBuildParameters.testingParameters.library == .xctest { + // Skip test discovery if preparing for indexing + if self.plan.destinationBuildParameters.prepareForIndexing == .off { try self.addTestDiscoveryGenerationCommand() + try self.addTestEntryPointGenerationCommand() } - try self.addTestEntryPointGenerationCommand() // Create command for all products in the plan. - for (_, description) in self.plan.productMap { - try self.createProductCommand(description) + for description in self.plan.productMap { + if description.buildParameters.prepareForIndexing == .off { + try self.createProductCommand(description) + } } try LLBuildManifestWriter.write(self.manifest, at: path, fileSystem: self.fileSystem) @@ -136,28 +134,11 @@ public class LLBuildManifestBuilder { extension LLBuildManifestBuilder { private func addPackageStructureCommand() { - let inputs = self.plan.graph.rootPackages.flatMap { package -> [Node] in - var inputs = package.targets - .map(\.sources.root) - .sorted() - .map { Node.directoryStructure($0) } - - // Add the output paths of any prebuilds that were run, so that we redo the plan if they change. - var derivedSourceDirPaths: [AbsolutePath] = [] - for result in self.plan.prebuildCommandResults.values.flatMap({ $0 }) { - derivedSourceDirPaths.append(contentsOf: result.outputDirectories) + let inputs = self.plan.inputs.map { + switch $0 { + case .directoryStructure(let path): return Node.directoryStructure(path) + case .file(let path): return Node.file(path) } - inputs.append(contentsOf: derivedSourceDirPaths.sorted().map { Node.directoryStructure($0) }) - - // FIXME: Need to handle version-specific manifests. - inputs.append(file: package.manifest.path) - - // FIXME: This won't be the location of Package.resolved for multiroot packages. - inputs.append(file: package.path.appending("Package.resolved")) - - // FIXME: Add config file as an input - - return inputs } let name = "PackageStructure" @@ -177,10 +158,13 @@ extension LLBuildManifestBuilder { extension LLBuildManifestBuilder { // Creates commands for copying all binary artifacts depended on in the plan. private func addBinaryDependencyCommands() { - // Make sure we don't have multiple copy commands for each destination by mapping each destination to + // Make sure we don't have multiple copy commands for each destination by mapping each destination to // its source binary. var destinations = [AbsolutePath: AbsolutePath]() for target in self.plan.targetMap.values { + // skip if target is preparing for indexing + guard target.buildParameters.prepareForIndexing == .off else { continue } + for binaryPath in target.libraryBinaryPaths { destinations[target.buildParameters.destinationPath(forBinaryAt: binaryPath)] = binaryPath } @@ -194,7 +178,12 @@ extension LLBuildManifestBuilder { // MARK: - Compilation extension LLBuildManifestBuilder { - func addBuildToolPlugins(_ target: TargetBuildDescription) throws { + func addBuildToolPlugins(_ target: ModuleBuildDescription) throws -> [Node] { + // For any build command that doesn't declare any outputs, we need to create a phony output to ensure they will still be run by the build system. + var phonyOutputs = [Node]() + // If we have multiple commands with no output files and no display name, this serves as a way to disambiguate the virtual nodes being created. + var pluginNumber = 1 + // Add any regular build commands created by plugins for the target. for result in target.buildToolPluginInvocationResults { // Only go through the regular build commands — prebuild commands are handled separately. @@ -213,17 +202,32 @@ extension LLBuildManifestBuilder { writableDirectories: [result.pluginOutputDirectory] ) } + let additionalOutputs: [Node] + if command.outputFiles.isEmpty { + if target.toolsVersion >= .v6_0 { + additionalOutputs = [.virtual("\(target.module.c99name)-\(command.configuration.displayName ?? "\(pluginNumber)")")] + phonyOutputs += additionalOutputs + } else { + additionalOutputs = [] + observabilityScope.emit(warning: "Build tool command '\(displayName)' (applied to target '\(target.module.name)') does not declare any output files and therefore will not run. You may want to consider updating the given package to tools-version 6.0 (or higher) which would run such a build tool command even without declared outputs.") + } + pluginNumber += 1 + } else { + additionalOutputs = [] + } self.manifest.addShellCmd( name: displayName + "-" + ByteString(encodingAsUTF8: uniquedName).sha256Checksum, description: displayName, inputs: command.inputFiles.map { .file($0) }, - outputs: command.outputFiles.map { .file($0) }, + outputs: command.outputFiles.map { .file($0) } + additionalOutputs, arguments: commandLine, environment: command.configuration.environment, workingDirectory: command.configuration.workingDirectory?.pathString ) } } + + return phonyOutputs } } @@ -233,7 +237,7 @@ extension LLBuildManifestBuilder { private func addTestDiscoveryGenerationCommand() throws { for testDiscoveryTarget in self.plan.targets.compactMap(\.testDiscoveryTargetBuildDescription) { let testTargets = testDiscoveryTarget.target.dependencies - .compactMap(\.target).compactMap { self.plan.targetMap[$0] } + .compactMap(\.module).compactMap { self.plan.description(for: $0, context: testDiscoveryTarget.destination) } let objectFiles = try testTargets.flatMap { try $0.objects }.sorted().map(Node.file) let outputs = testDiscoveryTarget.target.sources.paths @@ -250,29 +254,31 @@ extension LLBuildManifestBuilder { } private func addTestEntryPointGenerationCommand() throws { - for target in self.plan.targets { - guard case .swift(let target) = target, - case .entryPoint(let isSynthesized) = target.testTargetRole, + for module in self.plan.targets { + guard case .swift(let swiftModule) = module, + case .entryPoint(let isSynthesized) = swiftModule.testTargetRole, isSynthesized else { continue } - let testEntryPointTarget = target + let testEntryPointTarget = swiftModule - // Get the Swift target build descriptions of all discovery targets this synthesized entry point target + // Get the Swift target build descriptions of all discovery modules this synthesized entry point target // depends on. - let discoveredTargetDependencyBuildDescriptions = testEntryPointTarget.target.dependencies - .compactMap(\.target) - .compactMap { self.plan.targetMap[$0] } + let discoveredTargetDependencyBuildDescriptions = module.dependencies(using: self.plan) + .compactMap { + if case .module(_, let description) = $0 { + return description + } + return nil + } .compactMap(\.testDiscoveryTargetBuildDescription) - // The module outputs of the discovery targets this synthesized entry point target depends on are + // The module outputs of the discovery modules this synthesized entry point target depends on are // considered the inputs to the entry point command. let inputs = discoveredTargetDependencyBuildDescriptions.map(\.moduleOutputPath) let outputs = testEntryPointTarget.target.sources.paths - let mainFileName = TestEntryPointTool.mainFileName( - for: self.plan.destinationBuildParameters.testingParameters.library - ) + let mainFileName = TestEntryPointTool.mainFileName guard let mainOutput = (outputs.first { $0.basename == mainFileName }) else { throw InternalError("main output (\(mainFileName)) not found") } @@ -286,59 +292,31 @@ extension LLBuildManifestBuilder { } } -extension TargetBuildDescription { +extension ModuleBuildDescription { /// If receiver represents a Swift target build description whose test target role is Discovery, /// then this returns that Swift target build description, else returns nil. - fileprivate var testDiscoveryTargetBuildDescription: SwiftTargetBuildDescription? { + fileprivate var testDiscoveryTargetBuildDescription: SwiftModuleBuildDescription? { guard case .swift(let targetBuildDescription) = self, case .discovery = targetBuildDescription.testTargetRole else { return nil } return targetBuildDescription } } -extension ResolvedTarget { - public func getCommandName(config: String) -> String { - "C." + self.getLLBuildTargetName(config: config) - } - - public func getLLBuildTargetName(config: String) -> String { - "\(name)-\(config).module" - } - - public func getLLBuildResourcesCmdName(config: String) -> String { - "\(name)-\(config).module-resources" +extension ModuleBuildDescription { + package var llbuildResourcesCmdName: String { + "\(self.module.name)-\(self.buildParameters.triple.tripleString)-\(self.buildParameters.buildConfig)\(self.buildParameters.suffix).module-resources" } } -extension ResolvedProduct { - public func getLLBuildTargetName(config: String) throws -> String { - let potentialExecutableTargetName = "\(name)-\(config).exe" - let potentialLibraryTargetName = "\(name)-\(config).dylib" - - switch type { - case .library(.dynamic): - return potentialLibraryTargetName - case .test: - return "\(name)-\(config).test" - case .library(.static): - return "\(name)-\(config).a" - case .library(.automatic): - throw InternalError("automatic library not supported") - case .executable, .snippet: - return potentialExecutableTargetName - case .macro: - #if BUILD_MACROS_AS_DYLIBS - return potentialLibraryTargetName - #else - return potentialExecutableTargetName - #endif - case .plugin: - throw InternalError("unexpectedly asked for the llbuild target name of a plugin product") - } +extension ClangModuleBuildDescription { + package var llbuildTargetName: String { + self.target.getLLBuildTargetName(buildParameters: self.buildParameters) } +} - public func getCommandName(config: String) throws -> String { - try "C." + self.getLLBuildTargetName(config: config) +extension ResolvedModule { + public func getLLBuildTargetName(buildParameters: BuildParameters) -> String { + "\(self.name)-\(buildParameters.triple.tripleString)-\(buildParameters.buildConfig)\(buildParameters.suffix).module" } } diff --git a/Sources/Build/BuildOperation.swift b/Sources/Build/BuildOperation.swift index 6370ab2cc0a..ee3b48adfb6 100644 --- a/Sources/Build/BuildOperation.swift +++ b/Sources/Build/BuildOperation.swift @@ -10,25 +10,23 @@ // //===----------------------------------------------------------------------===// +import _Concurrency +@_spi(SwiftPMInternal) import Basics +import Foundation import LLBuildManifest import PackageGraph import PackageLoading import PackageModel import SPMBuildCore import SPMLLBuild -import Foundation +import class Basics.AsyncProcess import class TSCBasic.DiagnosticsEngine import protocol TSCBasic.OutputByteStream -import class TSCBasic.Process -import enum TSCBasic.ProcessEnv import struct TSCBasic.RegEx import enum TSCUtility.Diagnostics -import class TSCUtility.MultiLineNinjaProgressAnimation -import class TSCUtility.NinjaProgressAnimation -import protocol TSCUtility.ProgressAnimationProtocol #if USE_IMPL_ONLY_IMPORTS @_implementationOnly import DriverSupport @@ -38,27 +36,123 @@ import DriverSupport import SwiftDriver #endif +package struct LLBuildSystemConfiguration { + fileprivate let toolsBuildParameters: BuildParameters + fileprivate let destinationBuildParameters: BuildParameters + + let scratchDirectory: AbsolutePath + + let traitConfiguration: TraitConfiguration? + + fileprivate(set) var manifestPath: AbsolutePath + fileprivate(set) var databasePath: AbsolutePath + fileprivate(set) var buildDescriptionPath: AbsolutePath + + let fileSystem: any Basics.FileSystem + + let logLevel: Basics.Diagnostic.Severity + let outputStream: OutputByteStream + + let observabilityScope: ObservabilityScope + + init( + toolsBuildParameters: BuildParameters, + destinationBuildParameters: BuildParameters, + scratchDirectory: AbsolutePath, + traitConfiguration: TraitConfiguration?, + manifestPath: AbsolutePath? = nil, + databasePath: AbsolutePath? = nil, + buildDescriptionPath: AbsolutePath? = nil, + fileSystem: any Basics.FileSystem, + logLevel: Basics.Diagnostic.Severity, + outputStream: OutputByteStream, + observabilityScope: ObservabilityScope + ) { + self.toolsBuildParameters = toolsBuildParameters + self.destinationBuildParameters = destinationBuildParameters + self.scratchDirectory = scratchDirectory + self.traitConfiguration = traitConfiguration + self.manifestPath = manifestPath ?? destinationBuildParameters.llbuildManifest + self.databasePath = databasePath ?? scratchDirectory.appending("build.db") + self.buildDescriptionPath = buildDescriptionPath ?? destinationBuildParameters.buildDescriptionPath + self.fileSystem = fileSystem + self.logLevel = logLevel + self.outputStream = outputStream + self.observabilityScope = observabilityScope + } + + func buildParameters(for destination: BuildParameters.Destination) -> BuildParameters { + switch destination { + case .host: self.toolsBuildParameters + case .target: self.destinationBuildParameters + } + } + + func buildEnvironment(for destination: BuildParameters.Destination) -> BuildEnvironment { + switch destination { + case .host: self.toolsBuildParameters.buildEnvironment + case .target: self.destinationBuildParameters.buildEnvironment + } + } + + func shouldSkipBuilding(for destination: BuildParameters.Destination) -> Bool { + switch destination { + case .host: self.toolsBuildParameters.shouldSkipBuilding + case .target: self.destinationBuildParameters.shouldSkipBuilding + } + } + + func toolchain(for description: BuildParameters.Destination) -> any PackageModel.Toolchain { + switch description { + case .host: self.toolsBuildParameters.toolchain + case .target: self.destinationBuildParameters.toolchain + } + } + + func buildPath(for description: BuildParameters.Destination) -> AbsolutePath { + switch description { + case .host: self.toolsBuildParameters.buildPath + case .target: self.destinationBuildParameters.buildPath + } + } + + func dataPath(for description: BuildParameters.Destination) -> AbsolutePath { + switch description { + case .host: self.toolsBuildParameters.dataPath + case .target: self.destinationBuildParameters.dataPath + } + } + + func buildDescriptionPath(for description: BuildParameters.Destination) -> AbsolutePath { + switch description { + case .host: self.toolsBuildParameters.buildDescriptionPath + case .target: self.destinationBuildParameters.buildDescriptionPath + } + } + + func configuration(for destination: BuildParameters.Destination) -> BuildConfiguration { + switch destination { + case .host: self.toolsBuildParameters.configuration + case .target: self.destinationBuildParameters.configuration + } + } +} + public final class BuildOperation: PackageStructureDelegate, SPMBuildCore.BuildSystem, BuildErrorAdviceProvider { /// The delegate used by the build system. public weak var delegate: SPMBuildCore.BuildSystemDelegate? - /// Build parameters for products. - let productsBuildParameters: BuildParameters - - /// Build parameters for build tools: plugins and macros. - let toolsBuildParameters: BuildParameters + private let config: LLBuildSystemConfiguration /// The closure for loading the package graph. - let packageGraphLoader: () throws -> PackageGraph + let packageGraphLoader: () async throws -> ModulesGraph /// the plugin configuration for build plugins let pluginConfiguration: PluginConfiguration? - /// The llbuild build delegate reference. - private var buildSystemDelegate: BuildOperationBuildSystemDelegateHandler? - - /// The llbuild build system reference. - private var buildSystem: SPMLLBuild.BuildSystem? + /// The llbuild build system reference previously created + /// via `createBuildSystem` call. + private var current: (buildSystem: SPMLLBuild.BuildSystem, tracker: LLBuildProgressTracker)? /// If build manifest caching should be enabled. public let cacheBuildManifest: Bool @@ -80,22 +174,22 @@ public final class BuildOperation: PackageStructureDelegate, SPMBuildCore.BuildS private let buildDescription = ThreadSafeBox() /// The loaded package graph. - private let packageGraph = ThreadSafeBox() - - /// The output stream for the build delegate. - private let outputStream: OutputByteStream - - /// The verbosity level to use for diagnostics. - private let logLevel: Basics.Diagnostic.Severity + private let packageGraph = ThreadSafeBox() /// File system to operate on. - private let fileSystem: Basics.FileSystem + private var fileSystem: Basics.FileSystem { + config.fileSystem + } /// ObservabilityScope with which to emit diagnostics. - private let observabilityScope: ObservabilityScope + private var observabilityScope: ObservabilityScope { + config.observabilityScope + } public var builtTestProducts: [BuiltTestProduct] { - (try? getBuildDescription())?.builtTestProducts ?? [] + get async { + (try? await getBuildDescription())?.builtTestProducts ?? [] + } } /// File rules to determine resource handling behavior. @@ -104,42 +198,87 @@ public final class BuildOperation: PackageStructureDelegate, SPMBuildCore.BuildS /// Alternative path to search for pkg-config `.pc` files. private let pkgConfigDirectories: [AbsolutePath] - public init( + public var hasIntegratedAPIDigesterSupport: Bool { false } + + public convenience init( productsBuildParameters: BuildParameters, toolsBuildParameters: BuildParameters, cacheBuildManifest: Bool, - packageGraphLoader: @escaping () throws -> PackageGraph, + packageGraphLoader: @escaping () throws -> ModulesGraph, pluginConfiguration: PluginConfiguration? = .none, + scratchDirectory: AbsolutePath, additionalFileRules: [FileRuleDescription], pkgConfigDirectories: [AbsolutePath], outputStream: OutputByteStream, logLevel: Basics.Diagnostic.Severity, fileSystem: Basics.FileSystem, observabilityScope: ObservabilityScope + ) { + self.init( + productsBuildParameters: productsBuildParameters, + toolsBuildParameters: toolsBuildParameters, + cacheBuildManifest: cacheBuildManifest, + packageGraphLoader: packageGraphLoader, + pluginConfiguration: pluginConfiguration, + scratchDirectory: scratchDirectory, + traitConfiguration: nil, + additionalFileRules: additionalFileRules, + pkgConfigDirectories: pkgConfigDirectories, + outputStream: outputStream, + logLevel: logLevel, + fileSystem: fileSystem, + observabilityScope: observabilityScope, + delegate: nil + ) + } + + package init( + productsBuildParameters: BuildParameters, + toolsBuildParameters: BuildParameters, + cacheBuildManifest: Bool, + packageGraphLoader: @escaping () async throws -> ModulesGraph, + pluginConfiguration: PluginConfiguration? = .none, + scratchDirectory: AbsolutePath, + traitConfiguration: TraitConfiguration?, + additionalFileRules: [FileRuleDescription], + pkgConfigDirectories: [AbsolutePath], + outputStream: OutputByteStream, + logLevel: Basics.Diagnostic.Severity, + fileSystem: Basics.FileSystem, + observabilityScope: ObservabilityScope, + delegate: SPMBuildCore.BuildSystemDelegate? ) { /// Checks if stdout stream is tty. var productsBuildParameters = productsBuildParameters - productsBuildParameters.outputParameters.isColorized = outputStream.isTTY - + if productsBuildParameters.outputParameters.isColorized { + productsBuildParameters.outputParameters.isColorized = outputStream.isTTY + } var toolsBuildParameters = toolsBuildParameters - toolsBuildParameters.outputParameters.isColorized = outputStream.isTTY + if toolsBuildParameters.outputParameters.isColorized { + toolsBuildParameters.outputParameters.isColorized = outputStream.isTTY + } + self.config = LLBuildSystemConfiguration( + toolsBuildParameters: toolsBuildParameters, + destinationBuildParameters: productsBuildParameters, + scratchDirectory: scratchDirectory, + traitConfiguration: traitConfiguration, + fileSystem: fileSystem, + logLevel: logLevel, + outputStream: outputStream, + observabilityScope: observabilityScope.makeChildScope(description: "Build Operation") + ) - self.productsBuildParameters = productsBuildParameters - self.toolsBuildParameters = toolsBuildParameters self.cacheBuildManifest = cacheBuildManifest self.packageGraphLoader = packageGraphLoader self.additionalFileRules = additionalFileRules self.pluginConfiguration = pluginConfiguration self.pkgConfigDirectories = pkgConfigDirectories - self.outputStream = outputStream - self.logLevel = logLevel - self.fileSystem = fileSystem - self.observabilityScope = observabilityScope.makeChildScope(description: "Build Operation") + self.delegate = delegate } - public func getPackageGraph() throws -> PackageGraph { - try self.packageGraph.memoize { - try self.packageGraphLoader() + public func getPackageGraph() async throws -> ModulesGraph { + try await self.packageGraph.memoize { + try await self.packageGraphLoader() } } @@ -147,20 +286,26 @@ public final class BuildOperation: PackageStructureDelegate, SPMBuildCore.BuildS /// /// This will try skip build planning if build manifest caching is enabled /// and the package structure hasn't changed. - public func getBuildDescription() throws -> BuildDescription { - return try self.buildDescription.memoize { + public func getBuildDescription(subset: BuildSubset? = nil) async throws -> BuildDescription { + return try await self.buildDescription.memoize { if self.cacheBuildManifest { do { // if buildPackageStructure returns a valid description we use that, otherwise we perform full planning if try self.buildPackageStructure() { // confirm the step above created the build description as expected // we trust it to update the build description when needed - let buildDescriptionPath = self.productsBuildParameters.buildDescriptionPath + let buildDescriptionPath = self.config.buildDescriptionPath(for: .target) guard self.fileSystem.exists(buildDescriptionPath) else { throw InternalError("could not find build descriptor at \(buildDescriptionPath)") } // return the build description that's on disk. - return try BuildDescription.load(fileSystem: self.fileSystem, path: buildDescriptionPath) + let buildDescription = try BuildDescription.load(fileSystem: self.fileSystem, path: buildDescriptionPath) + + // We need to check that the build has same traits enabled for the cached build operation + // match otherwise we have to re-plan. + if buildDescription.traitConfiguration == self.config.traitConfiguration { + return buildDescription + } } } catch { // since caching is an optimization, warn about failing to load the cached version @@ -171,17 +316,17 @@ public final class BuildOperation: PackageStructureDelegate, SPMBuildCore.BuildS } } // We need to perform actual planning if we reach here. - return try self.plan().description + return try await self.generateDescription(subset: subset).description } } - public func getBuildManifest() throws -> LLBuildManifest { - return try self.plan().manifest + public func getBuildManifest() async throws -> LLBuildManifest { + try await self.generateDescription().manifest } /// Cancel the active build operation. public func cancel(deadline: DispatchTime) throws { - buildSystem?.cancel() + current?.buildSystem.cancel() } // Emit a warning if a target imports another target in this build @@ -194,7 +339,7 @@ public final class BuildOperation: PackageStructureDelegate, SPMBuildCore.BuildS // Ensure the compiler supports the import-scan operation guard DriverSupport.checkSupportedFrontendFlags( flags: ["import-prescan"], - toolchain: self.productsBuildParameters.toolchain, + toolchain: self.config.toolchain(for: .target), fileSystem: localFileSystem ) else { return @@ -216,7 +361,7 @@ public final class BuildOperation: PackageStructureDelegate, SPMBuildCore.BuildS let resolver = try ArgsResolver(fileSystem: localFileSystem) let executor = SPMSwiftDriverExecutor(resolver: resolver, fileSystem: localFileSystem, - env: ProcessEnv.vars) + env: Environment.current) let consumeDiagnostics: DiagnosticsEngine = DiagnosticsEngine(handlers: []) var driver = try Driver(args: commandLine, @@ -224,9 +369,9 @@ public final class BuildOperation: PackageStructureDelegate, SPMBuildCore.BuildS fileSystem: localFileSystem, executor: executor) guard !consumeDiagnostics.hasErrors else { - // If we could not init the driver with this command, something went wrong, - // proceed without checking this target. - continue + // If we could not init the driver with this command, something went wrong, + // proceed without checking this target. + continue } let imports = try driver.performImportPrescan().imports let nonDependencyTargetsSet = @@ -251,9 +396,14 @@ public final class BuildOperation: PackageStructureDelegate, SPMBuildCore.BuildS } /// Perform a build using the given build description and subset. - public func build(subset: BuildSubset) throws { - guard !self.productsBuildParameters.shouldSkipBuilding else { - return + public func build(subset: BuildSubset, buildOutputs: [BuildOutput]) async throws -> BuildResult { + var result = BuildResult( + serializedDiagnosticPathsByTargetName: .failure(StringError("Building was skipped")), + replArguments: nil, + ) + + guard !self.config.shouldSkipBuilding(for: .target) else { + return result } let buildStartTime = DispatchTime.now() @@ -261,38 +411,101 @@ public final class BuildOperation: PackageStructureDelegate, SPMBuildCore.BuildS // Get the build description (either a cached one or newly created). // Get the build description - let buildDescription = try getBuildDescription() + let buildDescription = try await self.getBuildDescription(subset: subset) // Verify dependency imports on the described targets try verifyTargetImports(in: buildDescription) // Create the build system. - let buildSystem = try self.createBuildSystem(buildDescription: buildDescription) - self.buildSystem = buildSystem + let (buildSystem, progressTracker) = try self.createBuildSystem( + buildDescription: buildDescription, + config: self.config + ) + self.current = (buildSystem, progressTracker) // If any plugins are part of the build set, compile them now to surface // any errors up-front. Returns true if we should proceed with the build // or false if not. It will already have thrown any appropriate error. - guard try self.compilePlugins(in: subset) else { - return + guard try await self.compilePlugins(in: subset) else { + result.serializedDiagnosticPathsByTargetName = .failure(StringError("Plugin compilation failed")) + return result } + let configuration = self.config.configuration(for: .target) // delegate is only available after createBuildSystem is called - self.buildSystemDelegate?.buildStart(configuration: self.productsBuildParameters.configuration) + progressTracker.buildStart(configuration: configuration) // Perform the build. - let llbuildTarget = try computeLLBuildTargetName(for: subset) + let llbuildTarget = try await computeLLBuildTargetName(for: subset) let success = buildSystem.build(target: llbuildTarget) let duration = buildStartTime.distance(to: .now()) - self.buildSystemDelegate?.buildComplete(success: success, duration: duration) - self.delegate?.buildSystem(self, didFinishWithResult: success) + let subsetDescriptor: String? + switch subset { + case .product(let productName, _): + subsetDescriptor = "product '\(productName)'" + case .target(let targetName, _): + subsetDescriptor = "target: '\(targetName)'" + case .allExcludingTests, .allIncludingTests: + subsetDescriptor = nil + } + + progressTracker.buildComplete( + success: success, + duration: duration, + subsetDescriptor: subsetDescriptor + ) guard success else { throw Diagnostics.fatalError } + let buildResultBuildPlan = buildOutputs.contains(.buildPlan) ? try buildPlan : nil + let buildResultReplArgs = buildOutputs.contains(.replArguments) ? try buildPlan.createREPLArguments() : nil + + let artifacts: [(String, PluginInvocationBuildResult.BuiltArtifact)]? + if buildOutputs.contains(.builtArtifacts) { + let builtProducts = try buildPlan.buildProducts + artifacts = try builtProducts.compactMap { + switch $0.product.type { + case .library(let kind): + let artifactKind: PluginInvocationBuildResult.BuiltArtifact.Kind + switch kind { + case .dynamic: artifactKind = .dynamicLibrary + case .static, .automatic: artifactKind = .staticLibrary + } + return try ($0.product.name, .init( + path: $0.binaryPath.pathString, + kind: artifactKind) + ) + case .executable: + return try ($0.product.name, .init(path: $0.binaryPath.pathString, kind: .executable)) + default: + return nil + } + } + } else { + artifacts = nil + } + + result = BuildResult( + serializedDiagnosticPathsByTargetName: result.serializedDiagnosticPathsByTargetName, + symbolGraph: result.symbolGraph, + buildPlan: buildResultBuildPlan, + replArguments: buildResultReplArgs, + builtArtifacts: artifacts + ) + var serializedDiagnosticPaths: [String: [AbsolutePath]] = [:] + do { + for module in try buildPlan.buildModules { + serializedDiagnosticPaths[module.module.name, default: []].append(contentsOf: module.diagnosticFiles) + } + result.serializedDiagnosticPathsByTargetName = .success(serializedDiagnosticPaths) + } catch { + result.serializedDiagnosticPathsByTargetName = .failure(error) + } + // Create backwards-compatibility symlink to old build path. - let oldBuildPath = productsBuildParameters.dataPath.parentDirectory.appending( - component: productsBuildParameters.configuration.dirname + let oldBuildPath = self.config.dataPath(for: .target).parentDirectory.appending( + component: configuration.dirname ) if self.fileSystem.exists(oldBuildPath) { do { try self.fileSystem.removeFileTree(oldBuildPath) } @@ -301,46 +514,53 @@ public final class BuildOperation: PackageStructureDelegate, SPMBuildCore.BuildS warning: "unable to delete \(oldBuildPath), skip creating symbolic link", underlyingError: error ) - return + + return result } } do { - try self.fileSystem.createSymbolicLink(oldBuildPath, pointingAt: productsBuildParameters.buildPath, relative: true) + try self.fileSystem.createSymbolicLink( + oldBuildPath, + pointingAt: self.config.buildPath(for: .target), + relative: true + ) } catch { self.observabilityScope.emit( warning: "unable to create symbolic link at \(oldBuildPath)", underlyingError: error ) } + + return result } /// Compiles any plugins specified or implied by the build subset, returning /// true if the build should proceed. Throws an error in case of failure. A /// reason why the build might not proceed even on success is if only plugins /// should be compiled. - func compilePlugins(in subset: BuildSubset) throws -> Bool { + func compilePlugins(in subset: BuildSubset) async throws -> Bool { // Figure out what, if any, plugin descriptions to compile, and whether // to continue building after that based on the subset. - let allPlugins = try getBuildDescription().pluginDescriptions - let pluginsToCompile: [PluginDescription] + let allPlugins = try await getBuildDescription().pluginDescriptions + let pluginsToCompile: [PluginBuildDescription] let continueBuilding: Bool switch subset { case .allExcludingTests, .allIncludingTests: pluginsToCompile = allPlugins continueBuilding = true - case .product(let productName): + case .product(let productName, _): pluginsToCompile = allPlugins.filter{ $0.productNames.contains(productName) } continueBuilding = pluginsToCompile.isEmpty - case .target(let targetName): - pluginsToCompile = allPlugins.filter{ $0.targetName == targetName } + case .target(let targetName, _): + pluginsToCompile = allPlugins.filter{ $0.moduleName == targetName } continueBuilding = pluginsToCompile.isEmpty } // Compile any plugins we ended up with. If any of them fails, it will // throw. for plugin in pluginsToCompile { - try compilePlugin(plugin) + try await compilePlugin(plugin) } // If we get this far they all succeeded. Return whether to continue the @@ -350,60 +570,64 @@ public final class BuildOperation: PackageStructureDelegate, SPMBuildCore.BuildS // Compiles a single plugin, emitting its output and throwing an error if it // fails. - func compilePlugin(_ plugin: PluginDescription) throws { + func compilePlugin(_ plugin: PluginBuildDescription) async throws { guard let pluginConfiguration else { throw InternalError("unknown plugin script runner") } // Compile the plugin, getting back a PluginCompilationResult. - class Delegate: PluginScriptCompilerDelegate { + final class Delegate: PluginScriptCompilerDelegate { let preparationStepName: String - let buildSystemDelegate: BuildOperationBuildSystemDelegateHandler? - init(preparationStepName: String, buildSystemDelegate: BuildOperationBuildSystemDelegateHandler?) { + let progressTracker: LLBuildProgressTracker? + init(preparationStepName: String, progressTracker: LLBuildProgressTracker?) { self.preparationStepName = preparationStepName - self.buildSystemDelegate = buildSystemDelegate + self.progressTracker = progressTracker } - func willCompilePlugin(commandLine: [String], environment: EnvironmentVariables) { - self.buildSystemDelegate?.preparationStepStarted(preparationStepName) + + func willCompilePlugin(commandLine: [String], environment: [String: String]) { + self.progressTracker?.preparationStepStarted(preparationStepName) } + func didCompilePlugin(result: PluginCompilationResult) { - self.buildSystemDelegate?.preparationStepHadOutput( + self.progressTracker?.preparationStepHadOutput( preparationStepName, output: result.commandLine.joined(separator: " "), verboseOnly: true ) if !result.compilerOutput.isEmpty { - self.buildSystemDelegate?.preparationStepHadOutput( + self.progressTracker?.preparationStepHadOutput( preparationStepName, output: result.compilerOutput, verboseOnly: false ) } - self.buildSystemDelegate?.preparationStepFinished(preparationStepName, result: (result.succeeded ? .succeeded : .failed)) + self.progressTracker?.preparationStepFinished(preparationStepName, result: (result.succeeded ? .succeeded : .failed)) } + func skippedCompilingPlugin(cachedResult: PluginCompilationResult) { // Historically we have emitted log info about cached plugins that are used. We should reconsider whether this is the right thing to do. - self.buildSystemDelegate?.preparationStepStarted(preparationStepName) + self.progressTracker?.preparationStepStarted(preparationStepName) if !cachedResult.compilerOutput.isEmpty { - self.buildSystemDelegate?.preparationStepHadOutput( + self.progressTracker?.preparationStepHadOutput( preparationStepName, output: cachedResult.compilerOutput, verboseOnly: false ) } - self.buildSystemDelegate?.preparationStepFinished(preparationStepName, result: (cachedResult.succeeded ? .succeeded : .failed)) + self.progressTracker?.preparationStepFinished(preparationStepName, result: (cachedResult.succeeded ? .succeeded : .failed)) } } - let delegate = Delegate(preparationStepName: "Compiling plugin \(plugin.targetName)", buildSystemDelegate: self.buildSystemDelegate) - let result = try temp_await { - pluginConfiguration.scriptRunner.compilePluginScript( - sourceFiles: plugin.sources.paths, - pluginName: plugin.targetName, - toolsVersion: plugin.toolsVersion, - observabilityScope: self.observabilityScope, - callbackQueue: DispatchQueue.sharedConcurrent, - delegate: delegate, - completion: $0) - } + let delegate = Delegate( + preparationStepName: "Compiling plugin \(plugin.moduleName)", + progressTracker: self.current?.tracker + ) + let result = try await pluginConfiguration.scriptRunner.compilePluginScript( + sourceFiles: plugin.sources.paths, + pluginName: plugin.moduleName, + toolsVersion: plugin.toolsVersion, + observabilityScope: self.observabilityScope, + callbackQueue: DispatchQueue.sharedConcurrent, + delegate: delegate + ) // Throw an error on failure; we will already have emitted the compiler's output in this case. if !result.succeeded { @@ -412,272 +636,277 @@ public final class BuildOperation: PackageStructureDelegate, SPMBuildCore.BuildS } /// Compute the llbuild target name using the given subset. - private func computeLLBuildTargetName(for subset: BuildSubset) throws -> String { + func computeLLBuildTargetName(for subset: BuildSubset) async throws -> String { + func inferTestDestination( + testModule: ResolvedModule, + graph: ModulesGraph + ) throws -> BuildParameters.Destination { + for product in graph.allProducts where product.type == .test { + if product.modules.contains(where: { $0.id == testModule.id }) { + return product.hasDirectMacroDependencies ? .host : .target + } + } + + throw InternalError("Could not find a product for test module: \(testModule)") + } + switch subset { case .allExcludingTests: return LLBuildManifestBuilder.TargetKind.main.targetName case .allIncludingTests: return LLBuildManifestBuilder.TargetKind.test.targetName - default: + case .product(let productName, let destination): // FIXME: This is super unfortunate that we might need to load the package graph. - let graph = try getPackageGraph() - if let result = subset.llbuildTargetName( - for: graph, - config: self.productsBuildParameters.configuration.dirname, - observabilityScope: self.observabilityScope - ) { - return result + let graph = try await getPackageGraph() + + let product = graph.product(for: productName) + + guard let product else { + observabilityScope.emit(error: "no product named '\(productName)'") + throw Diagnostics.fatalError } - throw Diagnostics.fatalError - } - } - /// Create the build plan and return the build description. - private func plan() throws -> (description: BuildDescription, manifest: LLBuildManifest) { - // Load the package graph. - let graph = try getPackageGraph() - - let buildToolPluginInvocationResults: [ResolvedTarget: [BuildToolPluginInvocationResult]] - let prebuildCommandResults: [ResolvedTarget: [PrebuildCommandResult]] - // Invoke any build tool plugins in the graph to generate prebuild commands and build commands. - if let pluginConfiguration, !self.productsBuildParameters.shouldSkipBuilding { - let buildOperationForPluginDependencies = BuildOperation( - productsBuildParameters: self.productsBuildParameters, - toolsBuildParameters: self.toolsBuildParameters, - cacheBuildManifest: false, - packageGraphLoader: { return graph }, - additionalFileRules: self.additionalFileRules, - pkgConfigDirectories: self.pkgConfigDirectories, - outputStream: self.outputStream, - logLevel: self.logLevel, - fileSystem: self.fileSystem, - observabilityScope: self.observabilityScope - ) - buildToolPluginInvocationResults = try graph.invokeBuildToolPlugins( - outputDir: pluginConfiguration.workDirectory.appending("outputs"), - builtToolsDir: self.toolsBuildParameters.buildPath, - buildEnvironment: self.toolsBuildParameters.buildEnvironment, - toolSearchDirectories: [self.toolsBuildParameters.toolchain.swiftCompilerPath.parentDirectory], - pkgConfigDirectories: self.pkgConfigDirectories, - sdkRootPath: self.toolsBuildParameters.toolchain.sdkRootPath, - pluginScriptRunner: pluginConfiguration.scriptRunner, - observabilityScope: self.observabilityScope, - fileSystem: self.fileSystem - ) { name, path in - try buildOperationForPluginDependencies.build(subset: .product(name)) - if let builtTool = try buildOperationForPluginDependencies.buildPlan.buildProducts.first(where: { $0.product.name == name}) { - return try builtTool.binaryPath - } else { - return nil - } + let buildParameters = if let destination { + config.buildParameters(for: destination) + } else if product.type == .macro || product.type == .plugin { + config.buildParameters(for: .host) + } else if product.type == .test { + config.buildParameters(for: product.hasDirectMacroDependencies ? .host : .target) + } else { + config.buildParameters(for: .target) + } + + // If the product is automatic, we build the main target because automatic products + // do not produce a binary right now. + if product.type == .library(.automatic) { + observabilityScope.emit( + warning: + "'--product' cannot be used with the automatic product '\(productName)'; building the default target instead" + ) + return LLBuildManifestBuilder.TargetKind.main.targetName } + return try product.getLLBuildTargetName(buildParameters: buildParameters) + case .target(let targetName, let destination): + // FIXME: This is super unfortunate that we might need to load the package graph. + let graph = try await getPackageGraph() + let module = graph.module(for: targetName) - // Surface any diagnostics from build tool plugins. - var succeeded = true - for (target, results) in buildToolPluginInvocationResults { - // There is one result for each plugin that gets applied to a target. - for result in results { - let diagnosticsEmitter = self.observabilityScope.makeDiagnosticsEmitter { - var metadata = ObservabilityMetadata() - metadata.targetName = target.name - metadata.pluginName = result.plugin.name - return metadata - } - for line in result.textOutput.split(separator: "\n") { - diagnosticsEmitter.emit(info: line) - } - for diag in result.diagnostics { - diagnosticsEmitter.emit(diag) - } - succeeded = succeeded && result.succeeded - } + guard let module else { + observabilityScope.emit(error: "no target named '\(targetName)'") + throw Diagnostics.fatalError + } - if !succeeded { - throw StringError("build stopped due to build-tool plugin failures") - } + let buildParameters = if let destination { + config.buildParameters(for: destination) + } else if module.type == .macro || module.type == .plugin { + config.buildParameters(for: .host) + } else if module.type == .test { + try config.buildParameters(for: inferTestDestination(testModule: module, graph: graph)) + } else { + config.buildParameters(for: .target) } - // Run any prebuild commands provided by build tool plugins. Any failure stops the build. - prebuildCommandResults = try graph.reachableTargets.reduce(into: [:], { partial, target in - partial[target] = try buildToolPluginInvocationResults[target].map { try self.runPrebuildCommands(for: $0) } - }) - } else { - buildToolPluginInvocationResults = [:] - prebuildCommandResults = [:] + return module.getLLBuildTargetName(buildParameters: buildParameters) } + } - // Emit warnings about any unhandled files in authored packages. We do this after applying build tool plugins, once we know what files they handled. - for package in graph.rootPackages where package.manifest.toolsVersion >= .v5_3 { - for target in package.targets { - // Get the set of unhandled files in targets. - var unhandledFiles = Set(target.underlying.others) - if unhandledFiles.isEmpty { continue } - - // Subtract out any that were inputs to any commands generated by plugins. - if let result = buildToolPluginInvocationResults[target] { - let handledFiles = result.flatMap{ $0.buildCommands.flatMap{ $0.inputFiles } } - unhandledFiles.subtract(handledFiles) - } - if unhandledFiles.isEmpty { continue } - - // Emit a diagnostic if any remain. This is kept the same as the previous message for now, but this could be improved. - let diagnosticsEmitter = self.observabilityScope.makeDiagnosticsEmitter { - var metadata = ObservabilityMetadata() - metadata.packageIdentity = package.identity - metadata.packageKind = package.manifest.packageKind - metadata.targetName = target.name - return metadata - } - var warning = "found \(unhandledFiles.count) file(s) which are unhandled; explicitly declare them as resources or exclude from the target\n" - for file in unhandledFiles { - warning += " " + file.pathString + "\n" - } - diagnosticsEmitter.emit(warning: warning) - } + package func generatePlan() async throws -> BuildPlan { + let graph = try await getPackageGraph() + + let pluginTools: [ResolvedModule.ID: [String: PluginTool]] + // FIXME: This is unfortunate but we need to build plugin tools upfront at the moment because + // llbuild doesn't support dynamic dependency detection. In order to construct a manifest + // we need to build and invoke all of the build-tool plugins and capture their outputs in + // `BuildPlan`. + if let pluginConfiguration: PluginConfiguration, !self.config.shouldSkipBuilding(for: .target) { + let pluginsPerModule = graph.pluginsPerModule( + satisfying: self.config.buildEnvironment(for: .host) + ) + + pluginTools = try await buildPluginTools( + graph: graph, + pluginsPerModule: pluginsPerModule, + hostTriple: try pluginConfiguration.scriptRunner.hostTriple + ) + } else { + pluginTools = [:] } - // Create the build plan based, on the graph and any information from plugins. - let plan = try BuildPlan( - productsBuildParameters: self.productsBuildParameters, - toolsBuildParameters: self.toolsBuildParameters, + // Create the build plan based on the modules graph and any information from plugins. + return try await BuildPlan( + destinationBuildParameters: self.config.buildParameters(for: .target), + toolsBuildParameters: self.config.buildParameters(for: .host), graph: graph, + pluginConfiguration: self.pluginConfiguration, + pluginTools: pluginTools, additionalFileRules: additionalFileRules, - buildToolPluginInvocationResults: buildToolPluginInvocationResults, - prebuildCommandResults: prebuildCommandResults, + pkgConfigDirectories: pkgConfigDirectories, disableSandbox: self.pluginConfiguration?.disableSandbox ?? false, fileSystem: self.fileSystem, observabilityScope: self.observabilityScope ) + } + + /// Create the build plan and return the build description. + private func generateDescription(subset: BuildSubset? = nil) async throws -> (description: BuildDescription, manifest: LLBuildManifest) { + let plan = try await generatePlan() self._buildPlan = plan + // Emit warnings about any unhandled files in authored packages. We do this after applying build tool plugins, once we know what files they handled. + // rdar://113256834 This fix works for the plugins that do not have PreBuildCommands. + let targetsToConsider: [ResolvedModule] + if let subset = subset, let recursiveDependencies = try + subset.recursiveDependencies(for: plan.graph, observabilityScope: observabilityScope) { + targetsToConsider = recursiveDependencies + } else { + targetsToConsider = Array(plan.graph.reachableModules) + } + + for module in targetsToConsider { + // Subtract out any that were inputs to any commands generated by plugins. + if let pluginResults = plan.buildToolPluginInvocationResults[module.id] { + diagnoseUnhandledFiles( + modulesGraph: plan.graph, + module: module, + buildToolPluginInvocationResults: pluginResults + ) + } + } + let (buildDescription, buildManifest) = try BuildDescription.create( - with: plan, - disableSandboxForPluginCommands: self.pluginConfiguration?.disableSandbox ?? false, - fileSystem: self.fileSystem, - observabilityScope: self.observabilityScope + from: plan, + using: self.config, + disableSandboxForPluginCommands: self.pluginConfiguration?.disableSandbox ?? false ) // Finally create the llbuild manifest from the plan. return (buildDescription, buildManifest) } + /// Emit warnings about any files that aren't specifically declared as resources + /// or excluded from the build of the given module. + private func diagnoseUnhandledFiles( + modulesGraph: ModulesGraph, + module: ResolvedModule, + buildToolPluginInvocationResults: [BuildToolPluginInvocationResult] + ) { + guard let package = modulesGraph.package(for: module), + package.manifest.toolsVersion >= .v5_3 + else { + return + } + + // Get the set of unhandled files in targets. + var unhandledFiles = Set(module.underlying.others) + if unhandledFiles.isEmpty { + return + } + + // Subtract out any that were inputs to any commands generated by plugins. + let handledFiles = buildToolPluginInvocationResults.flatMap { $0.buildCommands.flatMap(\.inputFiles) } + unhandledFiles.subtract(handledFiles) + + if unhandledFiles.isEmpty { + return + } + + // Emit a diagnostic if any remain. This is kept the same as the previous message for now, but this could be + // improved. + let diagnosticsEmitter = self.observabilityScope.makeDiagnosticsEmitter { + var metadata = ObservabilityMetadata() + metadata.packageIdentity = package.identity + metadata.packageKind = package.manifest.packageKind + metadata.moduleName = module.name + return metadata + } + var warning = + "found \(unhandledFiles.count) file(s) which are unhandled; explicitly declare them as resources or exclude from the target\n" + for file in unhandledFiles { + warning += " " + file.pathString + "\n" + } + diagnosticsEmitter.emit(warning: warning) + } + /// Build the package structure target. private func buildPackageStructure() throws -> Bool { - let buildSystem = try self.createBuildSystem(buildDescription: .none) - self.buildSystem = buildSystem + let (buildSystem, tracker) = try self.createBuildSystem( + buildDescription: .none, + config: self.config + ) + self.current = (buildSystem, tracker) // Build the package structure target which will re-generate the llbuild manifest, if necessary. - return buildSystem.build(target: "PackageStructure") + let buildSuccess = buildSystem.build(target: "PackageStructure") + + // If progress has been printed this will add a newline to separate it from what could be + // the output of the command. For instance `swift test --skip-build` may print progress for + // the Planning Build stage, followed immediately by a list of tests. Without this finialize() + // call the first entry in the list appear on the same line as the Planning Build progress. + tracker.finalize(success: true) + + return buildSuccess } /// Create the build system using the given build description. /// /// The build description should only be omitted when creating the build system for /// building the package structure target. - private func createBuildSystem(buildDescription: BuildDescription?) throws -> SPMLLBuild.BuildSystem { + private func createBuildSystem( + buildDescription: BuildDescription?, + config: LLBuildSystemConfiguration + ) throws -> (buildSystem: SPMLLBuild.BuildSystem, tracker: LLBuildProgressTracker) { // Figure out which progress bar we have to use during the build. - let progressAnimation: ProgressAnimationProtocol = self.logLevel.isVerbose - ? MultiLineNinjaProgressAnimation(stream: self.outputStream) - : NinjaProgressAnimation(stream: self.outputStream) - + let progressAnimation = ProgressAnimation.ninja( + stream: config.outputStream, + verbose: config.logLevel.isVerbose + ) let buildExecutionContext = BuildExecutionContext( - productsBuildParameters: self.productsBuildParameters, - toolsBuildParameters: self.toolsBuildParameters, + productsBuildParameters: config.destinationBuildParameters, + toolsBuildParameters: config.toolsBuildParameters, buildDescription: buildDescription, - fileSystem: self.fileSystem, - observabilityScope: self.observabilityScope, + fileSystem: config.fileSystem, + observabilityScope: config.observabilityScope, packageStructureDelegate: self, buildErrorAdviceProvider: self ) // Create the build delegate. - let buildSystemDelegate = BuildOperationBuildSystemDelegateHandler( + let progressTracker = LLBuildProgressTracker( buildSystem: self, buildExecutionContext: buildExecutionContext, - outputStream: self.outputStream, + outputStream: config.outputStream, progressAnimation: progressAnimation, - logLevel: self.logLevel, - observabilityScope: self.observabilityScope, + logLevel: config.logLevel, + observabilityScope: config.observabilityScope, delegate: self.delegate ) - self.buildSystemDelegate = buildSystemDelegate - - let databasePath = self.productsBuildParameters.dataPath.appending("build.db").pathString - let buildSystem = SPMLLBuild.BuildSystem( - buildFile: self.productsBuildParameters.llbuildManifest.pathString, - databaseFile: databasePath, - delegate: buildSystemDelegate, - schedulerLanes: self.productsBuildParameters.workers - ) - - // TODO: this seems fragile, perhaps we replace commandFailureHandler by adding relevant calls in the delegates chain - buildSystemDelegate.commandFailureHandler = { - buildSystem.cancel() - self.delegate?.buildSystemDidCancel(self) - } - return buildSystem - } - - /// Runs any prebuild commands associated with the given list of plugin invocation results, in order, and returns the - /// results of running those prebuild commands. - private func runPrebuildCommands(for pluginResults: [BuildToolPluginInvocationResult]) throws -> [PrebuildCommandResult] { - guard let pluginConfiguration = self.pluginConfiguration else { - throw InternalError("unknown plugin script runner") - - } - // Run through all the commands from all the plugin usages in the target. - return try pluginResults.map { pluginResult in - // As we go we will collect a list of prebuild output directories whose contents should be input to the build, - // and a list of the files in those directories after running the commands. - var derivedFiles: [AbsolutePath] = [] - var prebuildOutputDirs: [AbsolutePath] = [] - for command in pluginResult.prebuildCommands { - self.observabilityScope.emit(info: "Running" + (command.configuration.displayName ?? command.configuration.executable.basename)) - - // Run the command configuration as a subshell. This doesn't return until it is done. - // TODO: We need to also use any working directory, but that support isn't yet available on all platforms at a lower level. - var commandLine = [command.configuration.executable.pathString] + command.configuration.arguments - if !pluginConfiguration.disableSandbox { - commandLine = try Sandbox.apply(command: commandLine, fileSystem: self.fileSystem, strictness: .writableTemporaryDirectory, writableDirectories: [pluginResult.pluginOutputDirectory]) - } - let processResult = try Process.popen(arguments: commandLine, environment: command.configuration.environment) - let output = try processResult.utf8Output() + processResult.utf8stderrOutput() - if processResult.exitStatus != .terminated(code: 0) { - throw StringError("failed: \(command)\n\n\(output)") - } - - // Add any files found in the output directory declared for the prebuild command after the command ends. - let outputFilesDir = command.outputFilesDirectory - if let swiftFiles = try? self.fileSystem.getDirectoryContents(outputFilesDir).sorted() { - derivedFiles.append(contentsOf: swiftFiles.map{ outputFilesDir.appending(component: $0) }) - } - - // Add the output directory to the list of directories whose structure should affect the build plan. - prebuildOutputDirs.append(outputFilesDir) - } + let llbuildSystem = SPMLLBuild.BuildSystem( + buildFile: config.manifestPath.pathString, + databaseFile: config.databasePath.pathString, + delegate: progressTracker, + schedulerLanes: config.destinationBuildParameters.workers + ) - // Add the results of running any prebuild commands for this invocation. - return PrebuildCommandResult(derivedFiles: derivedFiles, outputDirectories: prebuildOutputDirs) - } + return (buildSystem: llbuildSystem, tracker: progressTracker) } public func provideBuildErrorAdvice(for target: String, command: String, message: String) -> String? { // Find the target for which the error was emitted. If we don't find it, we can't give any advice. - guard let _ = self._buildPlan?.targets.first(where: { $0.target.name == target }) else { return nil } + guard let _ = self._buildPlan?.targets.first(where: { $0.module.name == target }) else { return nil } // Check for cases involving modules that cannot be found. if let importedModule = try? RegEx(pattern: "no such module '(.+)'").matchGroups(in: message).first?.first { // A target is importing a module that can't be found. We take a look at the build plan and see if can offer any advice. // Look for a target with the same module name as the one that's being imported. - if let importedTarget = self._buildPlan?.targets.first(where: { $0.target.c99name == importedModule }) { + if let importedTarget = self._buildPlan?.targets.first(where: { $0.module.c99name == importedModule }) { // For the moment we just check for executables that other targets try to import. - if importedTarget.target.type == .executable { + if importedTarget.module.type == .executable { return "module '\(importedModule)' is the main module of an executable, and cannot be imported by tests and other targets" } - if importedTarget.target.type == .macro { + if importedTarget.module.type == .macro { return "module '\(importedModule)' is a macro, and cannot be imported by tests and other targets" } @@ -687,9 +916,9 @@ public final class BuildOperation: PackageStructureDelegate, SPMBuildCore.BuildS return nil } - public func packageStructureChanged() -> Bool { + public func packageStructureChanged() async -> Bool { do { - _ = try self.plan() + _ = try await self.generateDescription() } catch Diagnostics.fatalError { return false @@ -700,37 +929,130 @@ public final class BuildOperation: PackageStructureDelegate, SPMBuildCore.BuildS } return true } + + public func generatePIF(preserveStructure: Bool) async throws -> String { + throw StringError("PIF generation is not applicable to the native build system.") + } +} + +public struct PluginConfiguration { + /// Entity responsible for compiling and running plugin scripts. + let scriptRunner: PluginScriptRunner + + /// Directory where plugin intermediate files are stored. + let workDirectory: AbsolutePath + + /// Whether to sandbox commands from build tool plugins. + let disableSandbox: Bool + + public init( + scriptRunner: PluginScriptRunner, + workDirectory: AbsolutePath, + disableSandbox: Bool + ) { + self.scriptRunner = scriptRunner + self.workDirectory = workDirectory + self.disableSandbox = disableSandbox + } } extension BuildOperation { - public struct PluginConfiguration { - /// Entity responsible for compiling and running plugin scripts. - let scriptRunner: PluginScriptRunner + private func buildPluginTools( + graph: ModulesGraph, + pluginsPerModule: [ResolvedModule.ID: [ResolvedModule]], + hostTriple: Basics.Triple + ) async throws -> [ResolvedModule.ID: [String: PluginTool]] { + var accessibleToolsPerPlugin: [ResolvedModule.ID: [String: PluginTool]] = [:] - /// Directory where plugin intermediate files are stored. - let workDirectory: AbsolutePath + var config = self.config - /// Whether to sandbox commands from build tool plugins. - let disableSandbox: Bool + config.manifestPath = config.dataPath(for: .host).appending( + components: "..", "plugin-tools.yaml" + ) + + // FIXME: It should be possible to share database between plugin tools + // and regular builds. To make that happen we need to refactor + // `buildPackageStructure` to recognize the split. + config.databasePath = config.scratchDirectory.appending("plugin-tools.db") + + config.buildDescriptionPath = config.buildPath(for: .host).appending( + component: "plugin-tools-description.json" + ) + + let buildPlan = try await BuildPlan( + destinationBuildParameters: config.destinationBuildParameters, + toolsBuildParameters: config.toolsBuildParameters, + graph: graph, + pluginConfiguration: nil, + additionalFileRules: [], + fileSystem: config.fileSystem, + observabilityScope: config.observabilityScope + ) + + let (buildDescription, _) = try BuildDescription.create( + from: buildPlan, + using: config, + disableSandboxForPluginCommands: false + ) + + let (buildSystem, _) = try self.createBuildSystem( + buildDescription: buildDescription, + config: config + ) - public init(scriptRunner: PluginScriptRunner, workDirectory: AbsolutePath, disableSandbox: Bool) { - self.scriptRunner = scriptRunner - self.workDirectory = workDirectory - self.disableSandbox = disableSandbox + func buildToolBuilder(_ name: String, _ path: RelativePath) async throws -> AbsolutePath? { + let llbuildTarget = try await self.computeLLBuildTargetName(for: .product(name, for: .host)) + let success = buildSystem.build(target: llbuildTarget) + + if !success { + return nil + } + + return try buildPlan.buildProducts.first { + $0.product.name == name && $0.buildParameters.destination == .host + }?.binaryPath } + + for (_, plugins) in pluginsPerModule { + for plugin in plugins where accessibleToolsPerPlugin[plugin.id] == nil { + // Determine the tools to which this plugin has access, and create a name-to-path mapping from tool + // names to the corresponding paths. Built tools are assumed to be in the build tools directory. + let accessibleTools = try await plugin.preparePluginTools( + fileSystem: config.fileSystem, + environment: config.buildEnvironment(for: .host), + for: hostTriple + ) { name, path in + if let result = try await buildToolBuilder(name, path) { + return result + } else { + return config.buildPath(for: .host).appending(path) + } + } + + accessibleToolsPerPlugin[plugin.id] = accessibleTools + } + } + + return accessibleToolsPerPlugin } } extension BuildDescription { static func create( - with plan: BuildPlan, - disableSandboxForPluginCommands: Bool, - fileSystem: Basics.FileSystem, - observabilityScope: ObservabilityScope + from plan: BuildPlan, + using config: LLBuildSystemConfiguration, + disableSandboxForPluginCommands: Bool ) throws -> (BuildDescription, LLBuildManifest) { + let fileSystem = config.fileSystem + // Generate the llbuild manifest. - let llbuild = LLBuildManifestBuilder(plan, disableSandboxForPluginCommands: disableSandboxForPluginCommands, fileSystem: fileSystem, observabilityScope: observabilityScope) - let buildManifest = try llbuild.generateManifest(at: plan.destinationBuildParameters.llbuildManifest) + let llbuild = LLBuildManifestBuilder( + plan, + disableSandboxForPluginCommands: disableSandboxForPluginCommands, + fileSystem: fileSystem, + observabilityScope: config.observabilityScope + ) + let buildManifest = try llbuild.generateManifest(at: config.manifestPath) let swiftCommands = llbuild.manifest.getCmdToolMap(kind: SwiftCompilerTool.self) let swiftFrontendCommands = llbuild.manifest.getCmdToolMap(kind: SwiftFrontendTool.self) @@ -748,56 +1070,40 @@ extension BuildDescription { testEntryPointCommands: testEntryPointCommands, copyCommands: copyCommands, writeCommands: writeCommands, - pluginDescriptions: plan.pluginDescriptions + pluginDescriptions: plan.pluginDescriptions, + traitConfiguration: config.traitConfiguration ) try fileSystem.createDirectory( - plan.destinationBuildParameters.buildDescriptionPath.parentDirectory, + config.buildDescriptionPath.parentDirectory, recursive: true ) - try buildDescription.write(fileSystem: fileSystem, path: plan.destinationBuildParameters.buildDescriptionPath) + try buildDescription.write( + fileSystem: fileSystem, + path: config.buildDescriptionPath + ) return (buildDescription, buildManifest) } } extension BuildSubset { - /// Returns the name of the llbuild target that corresponds to the build subset. - func llbuildTargetName(for graph: PackageGraph, config: String, observabilityScope: ObservabilityScope) - -> String? - { + func recursiveDependencies(for graph: ModulesGraph, observabilityScope: ObservabilityScope) throws -> [ResolvedModule]? { switch self { - case .allExcludingTests: - return LLBuildManifestBuilder.TargetKind.main.targetName case .allIncludingTests: - return LLBuildManifestBuilder.TargetKind.test.targetName - case .product(let productName): - guard let product = graph.allProducts.first(where: { $0.name == productName }) else { + return Array(graph.reachableModules) + case .allExcludingTests: + return graph.reachableModules.filter { $0.type != .test } + case .product(let productName, _): + guard let product = graph.product(for: productName) else { observabilityScope.emit(error: "no product named '\(productName)'") return nil } - // If the product is automatic, we build the main target because automatic products - // do not produce a binary right now. - if product.type == .library(.automatic) { - observabilityScope.emit( - warning: - "'--product' cannot be used with the automatic product '\(productName)'; building the default target instead" - ) - return LLBuildManifestBuilder.TargetKind.main.targetName - } - return observabilityScope.trap { - try product.getLLBuildTargetName(config: config) - } - case .target(let targetName): - guard let target = graph.allTargets.first(where: { $0.name == targetName }) else { + return try product.recursiveModuleDependencies() + case .target(let targetName, _): + guard let target = graph.module(for: targetName) else { observabilityScope.emit(error: "no target named '\(targetName)'") return nil } - return target.getLLBuildTargetName(config: config) + return try target.recursiveModuleDependencies() } } } - -extension Basics.Diagnostic.Severity { - var isVerbose: Bool { - return self <= .info - } -} diff --git a/Sources/Build/BuildOperationBuildSystemDelegateHandler.swift b/Sources/Build/BuildOperationBuildSystemDelegateHandler.swift deleted file mode 100644 index 5832af110db..00000000000 --- a/Sources/Build/BuildOperationBuildSystemDelegateHandler.swift +++ /dev/null @@ -1,1146 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift open source project -// -// Copyright (c) 2018-2020 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 Basics -import Dispatch -import Foundation -import LLBuildManifest -import PackageModel -import SPMBuildCore -import SPMLLBuild - -import struct TSCBasic.ByteString -import struct TSCBasic.Format -import class TSCBasic.LocalFileOutputByteStream -import protocol TSCBasic.OutputByteStream -import enum TSCBasic.ProcessEnv -import struct TSCBasic.RegEx -import class TSCBasic.ThreadSafeOutputByteStream - -import class TSCUtility.IndexStore -import class TSCUtility.IndexStoreAPI -import protocol TSCUtility.ProgressAnimationProtocol - -#if canImport(llbuildSwift) -typealias LLBuildBuildSystemDelegate = llbuildSwift.BuildSystemDelegate -#else -typealias LLBuildBuildSystemDelegate = llbuild.BuildSystemDelegate -#endif - - -class CustomLLBuildCommand: SPMLLBuild.ExternalCommand { - let context: BuildExecutionContext - - required init(_ context: BuildExecutionContext) { - self.context = context - } - - func getSignature(_: SPMLLBuild.Command) -> [UInt8] { - [] - } - - func execute( - _: SPMLLBuild.Command, - _: SPMLLBuild.BuildSystemCommandInterface - ) -> Bool { - fatalError("subclass responsibility") - } -} - -extension IndexStore.TestCaseClass.TestMethod { - fileprivate var allTestsEntry: String { - let baseName = name.hasSuffix("()") ? String(name.dropLast(2)) : name - - return "(\"\(baseName)\", \(isAsync ? "asyncTest(\(baseName))" : baseName))" - } -} - -final class TestDiscoveryCommand: CustomLLBuildCommand, TestBuildCommand { - private func write( - tests: [IndexStore.TestCaseClass], - forModule module: String, - fileSystem: Basics.FileSystem, - path: AbsolutePath - ) throws { - - let testsByClassNames = Dictionary(grouping: tests, by: { $0.name }).sorted(by: { $0.key < $1.key }) - - var content = "import XCTest\n" - content += "@testable import \(module)\n" - - for iterator in testsByClassNames { - // 'className' provides uniqueness for derived class. - let className = iterator.key - let testMethods = iterator.value.flatMap(\.testMethods) - content += - #""" - - fileprivate extension \#(className) { - @available(*, deprecated, message: "Not actually deprecated. Marked as deprecated to allow inclusion of deprecated tests (which test deprecated functionality) without warnings") - static let __allTests__\#(className) = [ - \#(testMethods.map { $0.allTestsEntry }.joined(separator: ",\n ")) - ] - } - - """# - } - - content += - #""" - @available(*, deprecated, message: "Not actually deprecated. Marked as deprecated to allow inclusion of deprecated tests (which test deprecated functionality) without warnings") - func __\#(module)__allTests() -> [XCTestCaseEntry] { - return [ - \#(testsByClassNames.map { "testCase(\($0.key).__allTests__\($0.key))" } - .joined(separator: ",\n ")) - ] - } - """# - - try fileSystem.writeFileContents(path, string: content) - } - - private func execute(fileSystem: Basics.FileSystem, tool: TestDiscoveryTool) throws { - let outputs = tool.outputs.compactMap { try? AbsolutePath(validating: $0.name) } - - switch self.context.productsBuildParameters.testingParameters.library { - case .swiftTesting: - for file in outputs { - try fileSystem.writeIfChanged(path: file, string: "") - } - case .xctest: - let index = self.context.productsBuildParameters.indexStore - let api = try self.context.indexStoreAPI.get() - let store = try IndexStore.open(store: TSCAbsolutePath(index), api: api) - - // FIXME: We can speed this up by having one llbuild command per object file. - let tests = try store.listTests(in: tool.inputs.map { try TSCAbsolutePath(AbsolutePath(validating: $0.name)) }) - - let testsByModule = Dictionary(grouping: tests, by: { $0.module.spm_mangledToC99ExtendedIdentifier() }) - - // Find the main file path. - guard let mainFile = outputs.first(where: { path in - path.basename == TestDiscoveryTool.mainFileName - }) else { - throw InternalError("main output (\(TestDiscoveryTool.mainFileName)) not found") - } - - // Write one file for each test module. - // - // We could write everything in one file but that can easily run into type conflicts due - // in complex packages with large number of test targets. - for file in outputs where file != mainFile { - // FIXME: This is relying on implementation detail of the output but passing the - // the context all the way through is not worth it right now. - let module = file.basenameWithoutExt.spm_mangledToC99ExtendedIdentifier() - - guard let tests = testsByModule[module] else { - // This module has no tests so just write an empty file for it. - try fileSystem.writeFileContents(file, bytes: "") - continue - } - try write( - tests: tests, - forModule: module, - fileSystem: fileSystem, - path: file - ) - } - - let testsKeyword = tests.isEmpty ? "let" : "var" - - // Write the main file. - let stream = try LocalFileOutputByteStream(mainFile) - - stream.send( - #""" - import XCTest - - @available(*, deprecated, message: "Not actually deprecated. Marked as deprecated to allow inclusion of deprecated tests (which test deprecated functionality) without warnings") - public func __allDiscoveredTests() -> [XCTestCaseEntry] { - \#(testsKeyword) tests = [XCTestCaseEntry]() - - \#(testsByModule.keys.map { "tests += __\($0)__allTests()" }.joined(separator: "\n ")) - - return tests - } - """# - ) - - stream.flush() - } - } - - override func execute( - _ command: SPMLLBuild.Command, - _: SPMLLBuild.BuildSystemCommandInterface - ) -> Bool { - do { - // This tool will never run without the build description. - guard let buildDescription = self.context.buildDescription else { - throw InternalError("unknown build description") - } - guard let tool = buildDescription.testDiscoveryCommands[command.name] else { - throw InternalError("command \(command.name) not registered") - } - try execute(fileSystem: self.context.fileSystem, tool: tool) - return true - } catch { - self.context.observabilityScope.emit(error) - return false - } - } -} - -extension TestEntryPointTool { - public static func mainFileName(for library: BuildParameters.Testing.Library) -> String { - "runner-\(library).swift" - } -} - -final class TestEntryPointCommand: CustomLLBuildCommand, TestBuildCommand { - private func execute(fileSystem: Basics.FileSystem, tool: TestEntryPointTool) throws { - let outputs = tool.outputs.compactMap { try? AbsolutePath(validating: $0.name) } - - // Find the main output file - let mainFileName = TestEntryPointTool.mainFileName( - for: self.context.productsBuildParameters.testingParameters.library - ) - guard let mainFile = outputs.first(where: { path in - path.basename == mainFileName - }) else { - throw InternalError("main file output (\(mainFileName)) not found") - } - - // Write the main file. - let stream = try LocalFileOutputByteStream(mainFile) - - switch self.context.productsBuildParameters.testingParameters.library { - case .swiftTesting: - stream.send( - #""" - #if canImport(Testing) - import Testing - #endif - - @main struct Runner { - static func main() async { - #if canImport(Testing) - await Testing.__swiftPMEntryPoint() as Never - #endif - } - } - """# - ) - case .xctest: - // Find the inputs, which are the names of the test discovery module(s) - let inputs = tool.inputs.compactMap { try? AbsolutePath(validating: $0.name) } - let discoveryModuleNames = inputs.map(\.basenameWithoutExt) - - let testObservabilitySetup: String - let buildParameters = self.context.productsBuildParameters - if buildParameters.testingParameters.experimentalTestOutput && buildParameters.triple.supportsTestSummary { - testObservabilitySetup = "_ = SwiftPMXCTestObserver()\n" - } else { - testObservabilitySetup = "" - } - - stream.send( - #""" - \#(generateTestObservationCode(buildParameters: buildParameters)) - - import XCTest - \#(discoveryModuleNames.map { "import \($0)" }.joined(separator: "\n")) - - @main - @available(*, deprecated, message: "Not actually deprecated. Marked as deprecated to allow inclusion of deprecated tests (which test deprecated functionality) without warnings") - struct Runner { - static func main() { - \#(testObservabilitySetup) - XCTMain(__allDiscoveredTests()) as Never - } - } - """# - ) - } - - stream.flush() - } - - override func execute( - _ command: SPMLLBuild.Command, - _: SPMLLBuild.BuildSystemCommandInterface - ) -> Bool { - do { - // This tool will never run without the build description. - guard let buildDescription = self.context.buildDescription else { - throw InternalError("unknown build description") - } - guard let tool = buildDescription.testEntryPointCommands[command.name] else { - throw InternalError("command \(command.name) not registered") - } - try execute(fileSystem: self.context.fileSystem, tool: tool) - return true - } catch { - self.context.observabilityScope.emit(error) - return false - } - } -} - -private protocol TestBuildCommand {} - -private final class InProcessTool: Tool { - let context: BuildExecutionContext - let type: CustomLLBuildCommand.Type - - init(_ context: BuildExecutionContext, type: CustomLLBuildCommand.Type) { - self.context = context - self.type = type - } - - func createCommand(_: String) -> ExternalCommand? { - type.init(self.context) - } -} - -/// Contains the description of the build that is needed during the execution. -public struct BuildDescription: Codable { - public typealias CommandName = String - public typealias TargetName = String - public typealias CommandLineFlag = String - - /// The Swift compiler invocation targets. - let swiftCommands: [LLBuildManifest.CmdName: SwiftCompilerTool] - - /// The Swift compiler frontend invocation targets. - let swiftFrontendCommands: [LLBuildManifest.CmdName: SwiftFrontendTool] - - /// The map of test discovery commands. - let testDiscoveryCommands: [LLBuildManifest.CmdName: TestDiscoveryTool] - - /// The map of test entry point commands. - let testEntryPointCommands: [LLBuildManifest.CmdName: TestEntryPointTool] - - /// The map of copy commands. - let copyCommands: [LLBuildManifest.CmdName: CopyTool] - - /// The map of write commands. - let writeCommands: [LLBuildManifest.CmdName: WriteAuxiliaryFile] - - /// A flag that indicates this build should perform a check for whether targets only import - /// their explicitly-declared dependencies - let explicitTargetDependencyImportCheckingMode: BuildParameters.TargetDependencyImportCheckingMode - - /// Every target's set of dependencies. - let targetDependencyMap: [TargetName: [TargetName]] - - /// A full swift driver command-line invocation used to dependency-scan a given Swift target - let swiftTargetScanArgs: [TargetName: [CommandLineFlag]] - - /// A set of all targets with generated source - let generatedSourceTargetSet: Set - - /// The built test products. - public let builtTestProducts: [BuiltTestProduct] - - /// Distilled information about any plugins defined in the package. - let pluginDescriptions: [PluginDescription] - - public init( - plan: BuildPlan, - swiftCommands: [LLBuildManifest.CmdName: SwiftCompilerTool], - swiftFrontendCommands: [LLBuildManifest.CmdName: SwiftFrontendTool], - testDiscoveryCommands: [LLBuildManifest.CmdName: TestDiscoveryTool], - testEntryPointCommands: [LLBuildManifest.CmdName: TestEntryPointTool], - copyCommands: [LLBuildManifest.CmdName: CopyTool], - writeCommands: [LLBuildManifest.CmdName: WriteAuxiliaryFile], - pluginDescriptions: [PluginDescription] - ) throws { - self.swiftCommands = swiftCommands - self.swiftFrontendCommands = swiftFrontendCommands - self.testDiscoveryCommands = testDiscoveryCommands - self.testEntryPointCommands = testEntryPointCommands - self.copyCommands = copyCommands - self.writeCommands = writeCommands - self.explicitTargetDependencyImportCheckingMode = plan.destinationBuildParameters.driverParameters - .explicitTargetDependencyImportCheckingMode - self.targetDependencyMap = try plan.targets.reduce(into: [TargetName: [TargetName]]()) { partial, targetBuildDescription in - let deps = try targetBuildDescription.target.recursiveDependencies( - satisfying: plan.buildParameters(for: targetBuildDescription.target).buildEnvironment - ) - .compactMap(\.target).map(\.c99name) - partial[targetBuildDescription.target.c99name] = deps - } - var targetCommandLines: [TargetName: [CommandLineFlag]] = [:] - var generatedSourceTargets: [TargetName] = [] - for (target, description) in plan.targetMap { - guard case .swift(let desc) = description else { - continue - } - let buildParameters = plan.buildParameters(for: target) - targetCommandLines[target.c99name] = - try desc.emitCommandLine(scanInvocation: true) + [ - "-driver-use-frontend-path", buildParameters.toolchain.swiftCompilerPath.pathString - ] - if case .discovery = desc.testTargetRole { - generatedSourceTargets.append(target.c99name) - } - } - generatedSourceTargets.append( - contentsOf: plan.graph.allTargets.filter { $0.type == .plugin } - .map(\.c99name) - ) - self.swiftTargetScanArgs = targetCommandLines - self.generatedSourceTargetSet = Set(generatedSourceTargets) - self.builtTestProducts = try plan.buildProducts.filter { $0.product.type == .test }.map { desc in - return try BuiltTestProduct( - productName: desc.product.name, - binaryPath: desc.binaryPath, - packagePath: desc.package.path, - library: desc.buildParameters.testingParameters.library - ) - } - self.pluginDescriptions = pluginDescriptions - } - - public func write(fileSystem: Basics.FileSystem, path: AbsolutePath) throws { - let encoder = JSONEncoder.makeWithDefaults() - let data = try encoder.encode(self) - try fileSystem.writeFileContents(path, bytes: ByteString(data)) - } - - public static func load(fileSystem: Basics.FileSystem, path: AbsolutePath) throws -> BuildDescription { - let contents: Data = try fileSystem.readFileContents(path) - let decoder = JSONDecoder.makeWithDefaults() - return try decoder.decode(BuildDescription.self, from: contents) - } -} - -/// A provider of advice about build errors. -public protocol BuildErrorAdviceProvider { - /// Invoked after a command fails and an error message is detected in the output. Should return a string containing - /// advice or additional information, if any, based on the build plan. - func provideBuildErrorAdvice(for target: String, command: String, message: String) -> String? -} - -/// The context available during build execution. -public final class BuildExecutionContext { - /// Build parameters for products. - let productsBuildParameters: BuildParameters - - /// Build parameters for build tools. - let toolsBuildParameters: BuildParameters - - /// The build description. - /// - /// This is optional because we might not have a valid build description when performing the - /// build for PackageStructure target. - let buildDescription: BuildDescription? - - /// The package structure delegate. - let packageStructureDelegate: PackageStructureDelegate - - /// Optional provider of build error resolution advice. - let buildErrorAdviceProvider: BuildErrorAdviceProvider? - - let fileSystem: Basics.FileSystem - - let observabilityScope: ObservabilityScope - - public init( - productsBuildParameters: BuildParameters, - toolsBuildParameters: BuildParameters, - buildDescription: BuildDescription? = nil, - fileSystem: Basics.FileSystem, - observabilityScope: ObservabilityScope, - packageStructureDelegate: PackageStructureDelegate, - buildErrorAdviceProvider: BuildErrorAdviceProvider? = nil - ) { - self.productsBuildParameters = productsBuildParameters - self.toolsBuildParameters = toolsBuildParameters - self.buildDescription = buildDescription - self.fileSystem = fileSystem - self.observabilityScope = observabilityScope - self.packageStructureDelegate = packageStructureDelegate - self.buildErrorAdviceProvider = buildErrorAdviceProvider - } - - // MARK: - Private - - private var indexStoreAPICache = ThreadSafeBox>() - - /// Reference to the index store API. - var indexStoreAPI: Result { - self.indexStoreAPICache.memoize { - do { - #if os(Windows) - // The library's runtime component is in the `bin` directory on - // Windows rather than the `lib` directory as on Unix. The `lib` - // directory contains the import library (and possibly static - // archives) which are used for linking. The runtime component is - // not (necessarily) part of the SDK distributions. - // - // NOTE: the library name here `libIndexStore.dll` is technically - // incorrect as per the Windows naming convention. However, the - // library is currently installed as `libIndexStore.dll` rather than - // `IndexStore.dll`. In the future, this may require a fallback - // search, preferring `IndexStore.dll` over `libIndexStore.dll`. - let indexStoreLib = toolsBuildParameters.toolchain.swiftCompilerPath - .parentDirectory - .appending("libIndexStore.dll") - #else - let ext = toolsBuildParameters.triple.dynamicLibraryExtension - let indexStoreLib = try toolsBuildParameters.toolchain.toolchainLibDir - .appending("libIndexStore" + ext) - #endif - return try .success(IndexStoreAPI(dylib: TSCAbsolutePath(indexStoreLib))) - } catch { - return .failure(error) - } - } - } -} - -final class WriteAuxiliaryFileCommand: CustomLLBuildCommand { - override func getSignature(_ command: SPMLLBuild.Command) -> [UInt8] { - guard let buildDescription = self.context.buildDescription else { - self.context.observabilityScope.emit(error: "unknown build description") - return [] - } - guard let tool = buildDescription.writeCommands[command.name] else { - self.context.observabilityScope.emit(error: "command \(command.name) not registered") - return [] - } - - do { - let encoder = JSONEncoder.makeWithDefaults() - var hash = Data() - hash += try encoder.encode(tool.inputs) - hash += try encoder.encode(tool.outputs) - return [UInt8](hash) - } catch { - self.context.observabilityScope.emit(error: "getSignature() failed: \(error.interpolationDescription)") - return [] - } - } - - override func execute( - _ command: SPMLLBuild.Command, - _: SPMLLBuild.BuildSystemCommandInterface - ) -> Bool { - let outputFilePath: AbsolutePath - let tool: WriteAuxiliaryFile! - - do { - guard let buildDescription = self.context.buildDescription else { - throw InternalError("unknown build description") - } - guard let _tool = buildDescription.writeCommands[command.name] else { - throw StringError("command \(command.name) not registered") - } - tool = _tool - - guard let output = tool.outputs.first, output.kind == .file else { - throw StringError("invalid output path") - } - outputFilePath = try AbsolutePath(validating: output.name) - } catch { - self.context.observabilityScope.emit(error: "failed to write auxiliary file: \(error.interpolationDescription)") - return false - } - - do { - try self.context.fileSystem.writeIfChanged(path: outputFilePath, string: getFileContents(tool: tool)) - return true - } catch { - self.context.observabilityScope.emit(error: "failed to write auxiliary file '\(outputFilePath.pathString)': \(error.interpolationDescription)") - return false - } - } - - func getFileContents(tool: WriteAuxiliaryFile) throws -> String { - guard tool.inputs.first?.kind == .virtual, let generatedFileType = tool.inputs.first?.name.dropFirst().dropLast() else { - throw StringError("invalid inputs") - } - - for fileType in WriteAuxiliary.fileTypes { - if generatedFileType == fileType.name { - return try fileType.getFileContents(inputs: Array(tool.inputs.dropFirst())) - } - } - - throw InternalError("unhandled generated file type '\(generatedFileType)'") - } -} - -public protocol PackageStructureDelegate { - func packageStructureChanged() -> Bool -} - -final class PackageStructureCommand: CustomLLBuildCommand { - override func getSignature(_: SPMLLBuild.Command) -> [UInt8] { - let encoder = JSONEncoder.makeWithDefaults() - // Include build parameters and process env in the signature. - var hash = Data() - hash += try! encoder.encode(self.context.productsBuildParameters) - hash += try! encoder.encode(self.context.toolsBuildParameters) - hash += try! encoder.encode(ProcessEnv.vars) - return [UInt8](hash) - } - - override func execute( - _: SPMLLBuild.Command, - _: SPMLLBuild.BuildSystemCommandInterface - ) -> Bool { - self.context.packageStructureDelegate.packageStructureChanged() - } -} - -final class CopyCommand: CustomLLBuildCommand { - override func execute( - _ command: SPMLLBuild.Command, - _: SPMLLBuild.BuildSystemCommandInterface - ) -> Bool { - do { - // This tool will never run without the build description. - guard let buildDescription = self.context.buildDescription else { - throw InternalError("unknown build description") - } - guard let tool = buildDescription.copyCommands[command.name] else { - throw StringError("command \(command.name) not registered") - } - - let input = try AbsolutePath(validating: tool.inputs[0].name) - let output = try AbsolutePath(validating: tool.outputs[0].name) - try self.context.fileSystem.createDirectory(output.parentDirectory, recursive: true) - try self.context.fileSystem.removeFileTree(output) - try self.context.fileSystem.copy(from: input, to: output) - } catch { - self.context.observabilityScope.emit(error) - return false - } - return true - } -} - -/// Convenient llbuild build system delegate implementation -final class BuildOperationBuildSystemDelegateHandler: LLBuildBuildSystemDelegate, SwiftCompilerOutputParserDelegate { - private let outputStream: ThreadSafeOutputByteStream - private let progressAnimation: ProgressAnimationProtocol - var commandFailureHandler: (() -> Void)? - private let logLevel: Basics.Diagnostic.Severity - private weak var delegate: SPMBuildCore.BuildSystemDelegate? - private let buildSystem: SPMBuildCore.BuildSystem - private let queue = DispatchQueue(label: "org.swift.swiftpm.build-delegate") - private var taskTracker = CommandTaskTracker() - private var errorMessagesByTarget: [String: [String]] = [:] - private let observabilityScope: ObservabilityScope - private var cancelled: Bool = false - - /// Swift parsers keyed by llbuild command name. - private var swiftParsers: [String: SwiftCompilerOutputParser] = [:] - - /// Buffer to accumulate non-swift output until command is finished - private var nonSwiftMessageBuffers: [String: [UInt8]] = [:] - - /// The build execution context. - private let buildExecutionContext: BuildExecutionContext - - init( - buildSystem: SPMBuildCore.BuildSystem, - buildExecutionContext: BuildExecutionContext, - outputStream: OutputByteStream, - progressAnimation: ProgressAnimationProtocol, - logLevel: Basics.Diagnostic.Severity, - observabilityScope: ObservabilityScope, - delegate: SPMBuildCore.BuildSystemDelegate? - ) { - self.buildSystem = buildSystem - self.buildExecutionContext = buildExecutionContext - // FIXME: Implement a class convenience initializer that does this once they are supported - // https://forums.swift.org/t/allow-self-x-in-class-convenience-initializers/15924 - self.outputStream = outputStream as? ThreadSafeOutputByteStream ?? ThreadSafeOutputByteStream(outputStream) - self.progressAnimation = progressAnimation - self.logLevel = logLevel - self.observabilityScope = observabilityScope - self.delegate = delegate - - let swiftParsers = buildExecutionContext.buildDescription?.swiftCommands.mapValues { tool in - SwiftCompilerOutputParser(targetName: tool.moduleName, delegate: self) - } ?? [:] - self.swiftParsers = swiftParsers - - self.taskTracker.onTaskProgressUpdateText = { progressText, _ in - self.queue.async { - self.delegate?.buildSystem(self.buildSystem, didUpdateTaskProgress: progressText) - } - } - } - - // MARK: llbuildSwift.BuildSystemDelegate - - var fs: SPMLLBuild.FileSystem? { - nil - } - - func lookupTool(_ name: String) -> Tool? { - switch name { - case TestDiscoveryTool.name: - return InProcessTool(buildExecutionContext, type: TestDiscoveryCommand.self) - case TestEntryPointTool.name: - return InProcessTool(buildExecutionContext, type: TestEntryPointCommand.self) - case PackageStructureTool.name: - return InProcessTool(buildExecutionContext, type: PackageStructureCommand.self) - case CopyTool.name: - return InProcessTool(buildExecutionContext, type: CopyCommand.self) - case WriteAuxiliaryFile.name: - return InProcessTool(buildExecutionContext, type: WriteAuxiliaryFileCommand.self) - default: - return nil - } - } - - func hadCommandFailure() { - self.commandFailureHandler?() - } - - func handleDiagnostic(_ diagnostic: SPMLLBuild.Diagnostic) { - switch diagnostic.kind { - case .note: - self.observabilityScope.emit(info: diagnostic.message) - case .warning: - self.observabilityScope.emit(warning: diagnostic.message) - case .error: - self.observabilityScope.emit(error: diagnostic.message) - @unknown default: - self.observabilityScope.emit(info: diagnostic.message) - } - } - - func commandStatusChanged(_ command: SPMLLBuild.Command, kind: CommandStatusKind) { - guard !self.logLevel.isVerbose else { return } - guard command.shouldShowStatus else { return } - guard !swiftParsers.keys.contains(command.name) else { return } - - queue.async { - self.taskTracker.commandStatusChanged(command, kind: kind) - self.updateProgress() - } - } - - func commandPreparing(_ command: SPMLLBuild.Command) { - queue.async { - self.delegate?.buildSystem(self.buildSystem, willStartCommand: BuildSystemCommand(command)) - } - } - - func commandStarted(_ command: SPMLLBuild.Command) { - guard command.shouldShowStatus else { return } - - queue.async { - self.delegate?.buildSystem(self.buildSystem, didStartCommand: BuildSystemCommand(command)) - if self.logLevel.isVerbose { - self.outputStream.send("\(command.verboseDescription)\n") - self.outputStream.flush() - } - } - } - - func shouldCommandStart(_: SPMLLBuild.Command) -> Bool { - true - } - - func commandFinished(_ command: SPMLLBuild.Command, result: CommandResult) { - guard command.shouldShowStatus else { return } - guard !swiftParsers.keys.contains(command.name) else { return } - - queue.async { - if result == .cancelled { - self.cancelled = true - self.delegate?.buildSystemDidCancel(self.buildSystem) - } - - self.delegate?.buildSystem(self.buildSystem, didFinishCommand: BuildSystemCommand(command)) - - if !self.logLevel.isVerbose { - let targetName = self.swiftParsers[command.name]?.targetName - self.taskTracker.commandFinished(command, result: result, targetName: targetName) - self.updateProgress() - } - } - } - - func commandHadError(_ command: SPMLLBuild.Command, message: String) { - self.observabilityScope.emit(error: message) - } - - func commandHadNote(_ command: SPMLLBuild.Command, message: String) { - self.observabilityScope.emit(info: message) - } - - func commandHadWarning(_ command: SPMLLBuild.Command, message: String) { - self.observabilityScope.emit(warning: message) - } - - func commandCannotBuildOutputDueToMissingInputs( - _ command: SPMLLBuild.Command, - output: BuildKey, - inputs: [BuildKey] - ) { - self.observabilityScope.emit(.missingInputs(output: output, inputs: inputs)) - } - - func cannotBuildNodeDueToMultipleProducers(output: BuildKey, commands: [SPMLLBuild.Command]) { - self.observabilityScope.emit(.multipleProducers(output: output, commands: commands)) - } - - func commandProcessStarted(_ command: SPMLLBuild.Command, process: ProcessHandle) {} - - func commandProcessHadError(_ command: SPMLLBuild.Command, process: ProcessHandle, message: String) { - self.observabilityScope.emit(.commandError(command: command, message: message)) - } - - func commandProcessHadOutput(_ command: SPMLLBuild.Command, process: ProcessHandle, data: [UInt8]) { - guard command.shouldShowStatus else { return } - - if let swiftParser = swiftParsers[command.name] { - swiftParser.parse(bytes: data) - } else { - queue.async { - self.nonSwiftMessageBuffers[command.name, default: []] += data - } - } - } - - func commandProcessFinished( - _ command: SPMLLBuild.Command, - process: ProcessHandle, - result: CommandExtendedResult - ) { - queue.async { - if let buffer = self.nonSwiftMessageBuffers[command.name] { - self.progressAnimation.clear() - self.outputStream.send(buffer) - self.outputStream.flush() - self.nonSwiftMessageBuffers[command.name] = nil - } - } - - switch result.result { - case .cancelled: - self.cancelled = true - self.delegate?.buildSystemDidCancel(self.buildSystem) - case .failed: - // The command failed, so we queue up an asynchronous task to see if we have any error messages from the - // target to provide advice about. - queue.async { - guard let target = self.swiftParsers[command.name]?.targetName else { return } - guard let errorMessages = self.errorMessagesByTarget[target] else { return } - for errorMessage in errorMessages { - // Emit any advice that's provided for each error message. - if let adviceMessage = self.buildExecutionContext.buildErrorAdviceProvider?.provideBuildErrorAdvice( - for: target, - command: command.name, - message: errorMessage - ) { - self.outputStream.send("note: \(adviceMessage)\n") - self.outputStream.flush() - } - } - } - case .succeeded, .skipped: - break - @unknown default: - break - } - } - - func cycleDetected(rules: [BuildKey]) { - self.observabilityScope.emit(.cycleError(rules: rules)) - - queue.async { - self.delegate?.buildSystemDidDetectCycleInRules(self.buildSystem) - } - } - - func shouldResolveCycle(rules: [BuildKey], candidate: BuildKey, action: CycleAction) -> Bool { - false - } - - /// Invoked right before running an action taken before building. - func preparationStepStarted(_ name: String) { - queue.async { - self.taskTracker.buildPreparationStepStarted(name) - self.updateProgress() - } - } - - /// Invoked when an action taken before building emits output. - /// when verboseOnly is set to true, the output will only be printed in verbose logging mode - func preparationStepHadOutput(_ name: String, output: String, verboseOnly: Bool) { - queue.async { - self.progressAnimation.clear() - if !verboseOnly || self.logLevel.isVerbose { - self.outputStream.send("\(output.spm_chomp())\n") - self.outputStream.flush() - } - } - } - - /// Invoked right after running an action taken before building. The result - /// indicates whether the action succeeded, failed, or was cancelled. - func preparationStepFinished(_ name: String, result: CommandResult) { - queue.async { - self.taskTracker.buildPreparationStepFinished(name) - self.updateProgress() - } - } - - // MARK: SwiftCompilerOutputParserDelegate - - func swiftCompilerOutputParser(_ parser: SwiftCompilerOutputParser, didParse message: SwiftCompilerMessage) { - queue.async { - if self.logLevel.isVerbose { - if let text = message.verboseProgressText { - self.outputStream.send("\(text)\n") - self.outputStream.flush() - } - } else { - self.taskTracker.swiftCompilerDidOutputMessage(message, targetName: parser.targetName) - self.updateProgress() - } - - if let output = message.standardOutput { - // first we want to print the output so users have it handy - if !self.logLevel.isVerbose { - self.progressAnimation.clear() - } - - self.outputStream.send(output) - self.outputStream.flush() - - // next we want to try and scoop out any errors from the output (if reasonable size, otherwise this - // will be very slow), so they can later be passed to the advice provider in case of failure. - if output.utf8.count < 1024 * 10 { - let regex = try! RegEx(pattern: #".*(error:[^\n]*)\n.*"#, options: .dotMatchesLineSeparators) - for match in regex.matchGroups(in: output) { - self.errorMessagesByTarget[parser.targetName] = ( - self.errorMessagesByTarget[parser.targetName] ?? [] - ) + [match[0]] - } - } - } - } - } - - func swiftCompilerOutputParser(_ parser: SwiftCompilerOutputParser, didFailWith error: Error) { - let message = (error as? LocalizedError)?.errorDescription ?? error.localizedDescription - self.observabilityScope.emit(.swiftCompilerOutputParsingError(message)) - self.commandFailureHandler?() - } - - func buildStart(configuration: BuildConfiguration) { - queue.sync { - self.progressAnimation.clear() - self.outputStream.send("Building for \(configuration == .debug ? "debugging" : "production")...\n") - self.outputStream.flush() - } - } - - func buildComplete(success: Bool, duration: DispatchTimeInterval) { - queue.sync { - self.progressAnimation.complete(success: success) - if success { - let message = cancelled ? "Build cancelled!" : "Build complete!" - self.progressAnimation.clear() - self.outputStream.send("\(message) (\(duration.descriptionInSeconds))\n") - self.outputStream.flush() - } - } - } - - // MARK: Private - - private func updateProgress() { - if let progressText = taskTracker.latestFinishedText { - self.progressAnimation.update( - step: taskTracker.finishedCount, - total: taskTracker.totalCount, - text: progressText - ) - } - } -} - -/// Tracks tasks based on command status and swift compiler output. -private struct CommandTaskTracker { - private(set) var totalCount = 0 - private(set) var finishedCount = 0 - private var swiftTaskProgressTexts: [Int: String] = [:] - - /// The last task text before the task list was emptied. - private(set) var latestFinishedText: String? - - var onTaskProgressUpdateText: ((_ text: String, _ targetName: String?) -> Void)? - - mutating func commandStatusChanged(_ command: SPMLLBuild.Command, kind: CommandStatusKind) { - switch kind { - case .isScanning: - self.totalCount += 1 - case .isUpToDate: - self.totalCount -= 1 - case .isComplete: - self.finishedCount += 1 - @unknown default: - assertionFailure("unhandled command status kind \(kind) for command \(command)") - } - } - - mutating func commandFinished(_ command: SPMLLBuild.Command, result: CommandResult, targetName: String?) { - let progressTextValue = progressText(of: command, targetName: targetName) - self.onTaskProgressUpdateText?(progressTextValue, targetName) - - self.latestFinishedText = progressTextValue - } - - mutating func swiftCompilerDidOutputMessage(_ message: SwiftCompilerMessage, targetName: String) { - switch message.kind { - case .began(let info): - if let text = progressText(of: message, targetName: targetName) { - swiftTaskProgressTexts[info.pid] = text - onTaskProgressUpdateText?(text, targetName) - } - - totalCount += 1 - case .finished(let info): - if let progressText = swiftTaskProgressTexts[info.pid] { - latestFinishedText = progressText - swiftTaskProgressTexts[info.pid] = nil - } - - finishedCount += 1 - case .unparsableOutput, .signalled, .skipped: - break - } - } - - private func progressText(of command: SPMLLBuild.Command, targetName: String?) -> String { - // Transforms descriptions like "Linking ./.build/x86_64-apple-macosx/debug/foo" into "Linking foo". - if let firstSpaceIndex = command.description.firstIndex(of: " "), - let lastDirectorySeparatorIndex = command.description.lastIndex(of: "/") - { - let action = command.description[.. String? { - if case .began(let info) = message.kind { - switch message.name { - case "compile": - if let sourceFile = info.inputs.first { - let sourceFilePath = try! AbsolutePath(validating: sourceFile) - return "Compiling \(targetName) \(sourceFilePath.components.last!)" - } - case "link": - return "Linking \(targetName)" - case "merge-module": - return "Merging module \(targetName)" - case "emit-module": - return "Emitting module \(targetName)" - case "generate-dsym": - return "Generating \(targetName) dSYM" - case "generate-pch": - return "Generating \(targetName) PCH" - default: - break - } - } - - return nil - } - - mutating func buildPreparationStepStarted(_: String) { - self.totalCount += 1 - } - - mutating func buildPreparationStepFinished(_ name: String) { - latestFinishedText = name - self.finishedCount += 1 - } -} - -extension SwiftCompilerMessage { - fileprivate var verboseProgressText: String? { - switch kind { - case .began(let info): - return ([info.commandExecutable] + info.commandArguments).joined(separator: " ") - case .skipped, .finished, .signalled, .unparsableOutput: - return nil - } - } - - fileprivate var standardOutput: String? { - switch kind { - case .finished(let info), - .signalled(let info): - return info.output - case .unparsableOutput(let output): - return output - case .skipped, .began: - return nil - } - } -} - -extension Basics.Diagnostic { - fileprivate static func cycleError(rules: [BuildKey]) -> Self { - .error("build cycle detected: " + rules.map(\.key).joined(separator: ", ")) - } - - fileprivate static func missingInputs(output: BuildKey, inputs: [BuildKey]) -> Self { - let missingInputs = inputs.map(\.key).joined(separator: ", ") - return .error("couldn't build \(output.key) because of missing inputs: \(missingInputs)") - } - - fileprivate static func multipleProducers(output: BuildKey, commands: [SPMLLBuild.Command]) -> Self { - let producers = commands.map(\.description).joined(separator: ", ") - return .error("couldn't build \(output.key) because of multiple producers: \(producers)") - } - - fileprivate static func commandError(command: SPMLLBuild.Command, message: String) -> Self { - .error("command \(command.description) failed: \(message)") - } - - fileprivate static func swiftCompilerOutputParsingError(_ error: String) -> Self { - .error("failed parsing the Swift compiler output: \(error)") - } -} - -extension BuildSystemCommand { - fileprivate init(_ command: SPMLLBuild.Command) { - self.init( - name: command.name, - description: command.description, - verboseDescription: command.verboseDescription - ) - } -} diff --git a/Sources/Build/BuildPlan/BuildPlan+Clang.swift b/Sources/Build/BuildPlan/BuildPlan+Clang.swift index 40dcff1b776..fe9bc8f7619 100644 --- a/Sources/Build/BuildPlan/BuildPlan+Clang.swift +++ b/Sources/Build/BuildPlan/BuildPlan+Clang.swift @@ -10,40 +10,61 @@ // //===----------------------------------------------------------------------===// -import class PackageModel.BinaryTarget -import class PackageModel.ClangTarget -import class PackageModel.SwiftTarget -import class PackageModel.SystemLibraryTarget +import Basics +import PackageGraph +import PackageLoading +import SPMBuildCore + +import class PackageModel.BinaryModule +import class PackageModel.ClangModule +import class PackageModel.SwiftModule +import class PackageModel.SystemLibraryModule extension BuildPlan { /// Plan a Clang target. - func plan(clangTarget: ClangTargetBuildDescription) throws { - let dependencies = try clangTarget.target.recursiveDependencies(satisfying: clangTarget.buildEnvironment) + func plan(clangTarget: ClangModuleBuildDescription) throws { + let dependencies = clangTarget.recursiveDependencies(using: self) - for case .target(let dependency, _) in dependencies { + for case .module(let dependency, let description) in dependencies { switch dependency.underlying { - case is SwiftTarget: - if case let .swift(dependencyTargetDescription)? = targetMap[dependency] { + case is SwiftModule: + if case let .swift(dependencyTargetDescription)? = description { if let moduleMap = dependencyTargetDescription.moduleMap { - clangTarget.additionalFlags += ["-fmodule-map-file=\(moduleMap.pathString)"] + // C languages clients should either import the module or include the compatibility header next to it. + clangTarget.additionalFlags += ["-I", moduleMap.dirname] } } - case let target as ClangTarget where target.type == .library: + case let target as ClangModule where target.type == .library: // Setup search paths for C dependencies: clangTarget.additionalFlags += ["-I", target.includeDir.pathString] // Add the modulemap of the dependency if it has one. - if case let .clang(dependencyTargetDescription)? = targetMap[dependency] { + if case let .clang(dependencyTargetDescription)? = description { if let moduleMap = dependencyTargetDescription.moduleMap { clangTarget.additionalFlags += ["-fmodule-map-file=\(moduleMap.pathString)"] } } - case let target as SystemLibraryTarget: + case let target as SystemLibraryModule: clangTarget.additionalFlags += ["-fmodule-map-file=\(target.moduleMapPath.pathString)"] clangTarget.additionalFlags += try pkgConfig(for: target).cFlags - case let target as BinaryTarget: - if case .xcframework = target.kind { + case let target as BinaryModule: + switch target.kind { + case .unknown: + break + case .artifactsArchive: + let libraries = try self.parseLibraryArtifactsArchive(for: target, triple: clangTarget.buildParameters.triple) + for library in libraries { + library.headersPaths.forEach { + clangTarget.additionalFlags += ["-I", $0.pathString] + } + if let moduleMapPath = library.moduleMapPath { + clangTarget.additionalFlags += ["-fmodule-map-file=\(moduleMapPath)"] + } + + clangTarget.libraryBinaryPaths.insert(library.libraryPath) + } + case .xcframework: let libraries = try self.parseXCFramework(for: target, triple: clangTarget.buildParameters.triple) for library in libraries { library.headersPaths.forEach { @@ -52,9 +73,9 @@ extension BuildPlan { clangTarget.libraryBinaryPaths.insert(library.libraryPath) } } + default: continue } } } - } diff --git a/Sources/Build/BuildPlan/BuildPlan+Product.swift b/Sources/Build/BuildPlan/BuildPlan+Product.swift index bba44144e84..f7b1bb41231 100644 --- a/Sources/Build/BuildPlan/BuildPlan+Product.swift +++ b/Sources/Build/BuildPlan/BuildPlan+Product.swift @@ -11,28 +11,39 @@ //===----------------------------------------------------------------------===// import struct Basics.AbsolutePath -import struct Basics.Triple +import func Basics.depthFirstSearch import struct Basics.InternalError +import struct Basics.Triple +import struct PackageGraph.ResolvedModule import struct PackageGraph.ResolvedProduct -import struct PackageGraph.ResolvedTarget -import class PackageModel.BinaryTarget -import class PackageModel.ClangTarget -import class PackageModel.Target -import class PackageModel.SwiftTarget -import class PackageModel.SystemLibraryTarget +import class PackageModel.BinaryModule +import class PackageModel.ClangModule + +@_spi(SwiftPMInternal) +import class PackageModel.Module + +import class PackageModel.SwiftModule +import class PackageModel.SystemLibraryModule import struct SPMBuildCore.BuildParameters import struct SPMBuildCore.ExecutableInfo +import struct SPMBuildCore.LibraryInfo import func TSCBasic.topologicalSort extension BuildPlan { /// Plan a product. func plan(buildProduct: ProductBuildDescription) throws { // Compute the product's dependency. - let dependencies = try computeDependencies(of: buildProduct.product, buildParameters: buildProduct.buildParameters) + let dependencies = try computeDependencies(of: buildProduct) + + var isEmbeddedSwift = false + for module in dependencies.staticTargets { + guard case .swift(let module) = module else { continue } + isEmbeddedSwift = isEmbeddedSwift || module.isEmbeddedSwift + } // Add flags for system targets. for systemModule in dependencies.systemModules { - guard case let target as SystemLibraryTarget = systemModule.underlying else { + guard case let target as SystemLibraryModule = systemModule.underlying else { throw InternalError("This should not be possible.") } // Add pkgConfig libs arguments. @@ -46,32 +57,37 @@ extension BuildPlan { } else if binaryPath.basename.starts(with: "lib") { buildProduct.additionalFlags += ["-l\(binaryPath.basenameWithoutExt.dropFirst(3))"] } else { - self.observabilityScope.emit(error: "unexpected binary framework") + self.observabilityScope.emit(error: "unexpected binary name at \(binaryPath). Static libraries should be prefixed with lib") } } - // Link C++ if needed. - // Note: This will come from build settings in future. - for target in dependencies.staticTargets { - if case let target as ClangTarget = target.underlying, target.isCXX { - let triple = buildProduct.buildParameters.triple - if triple.isDarwin() { - buildProduct.additionalFlags += ["-lc++"] - } else if triple.isWindows() { - // Don't link any C++ library. - } else { - buildProduct.additionalFlags += ["-lstdc++"] + // Don't link libc++ or libstd++ when building for Embedded Swift. + // Users can still link it manually for embedded platforms when needed, + // by providing `-Xlinker -lc++` options via CLI or `Package.swift`. + if !isEmbeddedSwift { + // Link C++ if needed. + // Note: This will come from build settings in future. + for description in dependencies.staticTargets { + if case let target as ClangModule = description.module.underlying, target.isCXX { + let triple = buildProduct.buildParameters.triple + if triple.isDarwin() || triple.isFreeBSD() { + buildProduct.additionalFlags += ["-lc++"] + } else if triple.isWindows() { + // Don't link any C++ library. + } else { + buildProduct.additionalFlags += ["-lstdc++"] + } + break } - break } } - for target in dependencies.staticTargets { - switch target.underlying { - case is SwiftTarget: + for description in dependencies.staticTargets { + switch description.module.underlying { + case is SwiftModule: // Swift targets are guaranteed to have a corresponding Swift description. - guard case .swift(let description) = targetMap[target] else { - throw InternalError("unknown target \(target)") + guard case .swift(let description) = description else { + throw InternalError("Expected a Swift module: \(description.module)") } // Based on the debugging strategy, we either need to pass swiftmodule paths to the @@ -90,178 +106,257 @@ extension BuildPlan { } } - buildProduct.staticTargets = dependencies.staticTargets - buildProduct.dylibs = try dependencies.dylibs.map{ - guard let product = productMap[$0] else { - throw InternalError("unknown product \($0)") - } - return product - } - buildProduct.objects += try dependencies.staticTargets.flatMap { targetName -> [AbsolutePath] in - guard let target = targetMap[targetName] else { - throw InternalError("unknown target \(targetName)") - } - return try target.objects - } + buildProduct.staticTargets = dependencies.staticTargets.map(\.module) + buildProduct.dylibs = dependencies.dylibs + buildProduct.objects += try dependencies.staticTargets.flatMap { try $0.objects } buildProduct.libraryBinaryPaths = dependencies.libraryBinaryPaths - buildProduct.availableTools = dependencies.availableTools } /// Computes the dependencies of a product. private func computeDependencies( - of product: ResolvedProduct, - buildParameters: BuildParameters + of productDescription: ProductBuildDescription ) throws -> ( - dylibs: [ResolvedProduct], - staticTargets: [ResolvedTarget], - systemModules: [ResolvedTarget], + dylibs: [ProductBuildDescription], + staticTargets: [ModuleBuildDescription], + systemModules: [ResolvedModule], libraryBinaryPaths: Set, availableTools: [String: AbsolutePath] ) { + let product = productDescription.product /* Prior to tools-version 5.9, we used to erroneously recursively traverse executable/plugin dependencies and statically include their targets. For compatibility reasons, we preserve that behavior for older tools-versions. */ - let shouldExcludePlugins: Bool - if let toolsVersion = self.graph.package(for: product)?.manifest.toolsVersion { - shouldExcludePlugins = toolsVersion >= .v5_9 - } else { - shouldExcludePlugins = false - } + let shouldExcludePlugins = productDescription.package.manifest.toolsVersion >= .v5_9 - // For test targets, we need to consider the first level of transitive dependencies since the first level is always test targets. - let topLevelDependencies: [PackageModel.Target] - if product.type == .test { - topLevelDependencies = product.targets.flatMap { $0.underlying.dependencies }.compactMap { + // For test targets, we need to consider the first level of transitive dependencies since the first level is + // always test targets. + let topLevelDependencies: [PackageModel.Module] = if product.type == .test { + product.modules.flatMap(\.underlying.dependencies).compactMap { switch $0 { case .product: - return nil - case .target(let target, _): - return target + nil + case .module(let target, _): + target } } } else { - topLevelDependencies = [] + [] + } + + // get the dynamic libraries for explicitly linking rdar://108561857 + func recursiveDynamicLibraries(for description: ProductBuildDescription) throws -> [ProductBuildDescription] { + let dylibs = try computeDependencies(of: description).dylibs + return try dylibs + dylibs.flatMap { try recursiveDynamicLibraries(for: $0) } } // Sort the product targets in topological order. - let nodes: [ResolvedTarget.Dependency] = product.targets.map { .target($0, conditions: []) } - let allTargets = try topologicalSort(nodes, successors: { dependency in - switch dependency { - // Include all the dependencies of a target. - case .target(let target, _): - let isTopLevel = topLevelDependencies.contains(target.underlying) || product.targets.contains(target) + var allDependencies: [ModuleBuildDescription.Dependency] = [] + + do { + func successors( + for product: ResolvedProduct, + destination: BuildParameters.Destination + ) throws -> [TraversalNode] { + let productDependencies: [TraversalNode] = product.modules.map { + .init(module: $0, context: destination) + } + + switch product.type { + case .library(.automatic), .library(.static): + return productDependencies + case .plugin: + return shouldExcludePlugins ? [] : productDependencies + case .library(.dynamic): + guard let description = self.description(for: product, context: destination) else { + throw InternalError("Could not find a description for product: \(product)") + } + return try recursiveDynamicLibraries(for: description).map { TraversalNode( + product: $0.product, + context: $0.destination + ) } + case .test, .executable, .snippet, .macro: + return [] + } + } + + func successors( + for module: ResolvedModule, + destination: BuildParameters.Destination + ) -> [TraversalNode] { + let isTopLevel = topLevelDependencies.contains(module.underlying) || product.modules + .contains(id: module.id) let topLevelIsMacro = isTopLevel && product.type == .macro let topLevelIsPlugin = isTopLevel && product.type == .plugin let topLevelIsTest = isTopLevel && product.type == .test - if !topLevelIsMacro && !topLevelIsTest && target.type == .macro { + if !topLevelIsMacro && !topLevelIsTest && module.type == .macro { return [] } - if shouldExcludePlugins, !topLevelIsPlugin && !topLevelIsTest && target.type == .plugin { + if shouldExcludePlugins, !topLevelIsPlugin && !topLevelIsTest && module.type == .plugin { return [] } - return target.dependencies.filter { $0.satisfies(buildParameters.buildEnvironment) } + return module.dependencies(satisfying: productDescription.buildParameters.buildEnvironment) + .map { + switch $0 { + case .product(let product, _): + .init(product: product, context: destination) + case .module(let module, _): + .init(module: module, context: destination) + } + } + } - // For a product dependency, we only include its content only if we - // need to statically link it. - case .product(let product, _): - guard dependency.satisfies(buildParameters.buildEnvironment) else { - return [] + let directDependencies = product.modules + .map { TraversalNode(module: $0, context: productDescription.destination) } + + var uniqueNodes = Set(directDependencies) + + try depthFirstSearch(directDependencies) { + let result: [TraversalNode] = switch $0 { + case .product(let product, let destination): + try successors(for: product, destination: destination) + case .module(let module, let destination): + successors(for: module, destination: destination) + case .package: + [] } - let productDependencies: [ResolvedTarget.Dependency] = product.targets.map { .target($0, conditions: []) } - switch product.type { - case .library(.automatic), .library(.static): - return productDependencies - case .plugin: - return shouldExcludePlugins ? [] : productDependencies - case .library(.dynamic), .test, .executable, .snippet, .macro: - return [] + return result.filter { uniqueNodes.insert($0).inserted } + } onNext: { node, _ in + switch node { + case .package: break + case .product(let product, let destination): + allDependencies.append(.product(product, self.description(for: product, context: destination))) + case .module(let module, let destination): + allDependencies.append(.module(module, self.description(for: module, context: destination))) } } - }) + } // Create empty arrays to collect our results. - var linkLibraries = [ResolvedProduct]() - var staticTargets = [ResolvedTarget]() - var systemModules = [ResolvedTarget]() + var linkLibraries = [ProductBuildDescription]() + var staticTargets = [ModuleBuildDescription]() + var systemModules = [ResolvedModule]() var libraryBinaryPaths: Set = [] var availableTools = [String: AbsolutePath]() - for dependency in allTargets { + for dependency in allDependencies { switch dependency { - case .target(let target, _): - switch target.type { + case .module(let module, let description): + switch module.type { // Executable target have historically only been included if they are directly in the product's // target list. Otherwise they have always been just build-time dependencies. // In tool version .v5_5 or greater, we also include executable modules implemented in Swift in // any test products... this is to allow testing of executables. Note that they are also still // built as separate products that the test can invoke as subprocesses. case .executable, .snippet, .macro: - if product.targets.contains(target) { - staticTargets.append(target) - } else if product.type == .test && (target.underlying as? SwiftTarget)?.supportsTestableExecutablesFeature == true { + if product.modules.contains(id: module.id) { + guard let description else { + throw InternalError("Could not find a description for module: \(module)") + } + staticTargets.append(description) + } else if product.type == .test && (module.underlying as? SwiftModule)? + .supportsTestableExecutablesFeature == true + { // Only "top-level" targets should really be considered here, not transitive ones. - let isTopLevel = topLevelDependencies.contains(target.underlying) || product.targets.contains(target) - if let toolsVersion = graph.package(for: product)?.manifest.toolsVersion, toolsVersion >= .v5_5, isTopLevel { - staticTargets.append(target) + let isTopLevel = topLevelDependencies.contains(module.underlying) || product.modules + .contains(id: module.id) + if let toolsVersion = graph.package(for: product)?.manifest.toolsVersion, toolsVersion >= .v5_5, + isTopLevel + { + guard let description else { + throw InternalError("Could not find a description for module: \(module)") + } + staticTargets.append(description) } } // Test targets should be included only if they are directly in the product's target list. case .test: - if product.targets.contains(target) { - staticTargets.append(target) + if product.modules.contains(id: module.id) { + guard let description else { + throw InternalError("Could not find a description for module: \(module)") + } + staticTargets.append(description) } - // Library targets should always be included. + // Library targets should always be included for the same build triple. case .library: - staticTargets.append(target) + guard let description else { + throw InternalError("Could not find a description for module: \(module)") + } + if description.destination == productDescription.destination { + staticTargets.append(description) + } // Add system target to system targets array. case .systemModule: - systemModules.append(target) + systemModules.append(module) // Add binary to binary paths set. case .binary: - guard let binaryTarget = target.underlying as? BinaryTarget else { - throw InternalError("invalid binary target '\(target.name)'") + guard let binaryTarget = module.underlying as? BinaryModule else { + throw InternalError("invalid binary target '\(module.name)'") } switch binaryTarget.kind { case .xcframework: - let libraries = try self.parseXCFramework(for: binaryTarget, triple: buildParameters.triple) + let libraries = try self.parseXCFramework( + for: binaryTarget, + triple: productDescription.buildParameters.triple + ) for library in libraries { libraryBinaryPaths.insert(library.libraryPath) } case .artifactsArchive: - let tools = try self.parseArtifactsArchive(for: binaryTarget, triple: buildParameters.triple) - tools.forEach { availableTools[$0.name] = $0.executablePath } - case.unknown: - throw InternalError("unknown binary target '\(target.name)' type") + let tools = try self.parseExecutableArtifactsArchive( + for: binaryTarget, triple: productDescription.buildParameters.triple + ) + for tool in tools { + availableTools[tool.name] = tool.executablePath + } + + let libraries = try self.parseLibraryArtifactsArchive( + for: binaryTarget, + triple: productDescription.buildParameters.triple + ) + for library in libraries { + libraryBinaryPaths.insert(library.libraryPath) + } + case .unknown: + throw InternalError("unknown binary target '\(module.name)' type") } case .plugin: continue } - case .product(let product, _): + case .product(let product, let description): // Add the dynamic products to array of libraries to link. if product.type == .library(.dynamic) { - linkLibraries.append(product) + guard let description else { + throw InternalError("Dynamic library product should have description: \(product)") + } + linkLibraries.append(description) } } } // Add derived test targets, if necessary - if buildParameters.testingParameters.testProductStyle.requiresAdditionalDerivedTestTargets { - if product.type == .test, let derivedTestTargets = derivedTestTargetsMap[product] { - staticTargets.append(contentsOf: derivedTestTargets) - } + if product.type == .test, let derivedTestTargets = derivedTestTargetsMap[product.id] { + staticTargets.append(contentsOf: derivedTestTargets.compactMap { + self.description(for: $0, context: productDescription.destination) + }) } return (linkLibraries, staticTargets, systemModules, libraryBinaryPaths, availableTools) } /// Extracts the artifacts from an artifactsArchive - private func parseArtifactsArchive(for binaryTarget: BinaryTarget, triple: Triple) throws -> [ExecutableInfo] { - try self.externalExecutablesCache.memoize(key: binaryTarget) { - let execInfos = try binaryTarget.parseArtifactArchives(for: triple, fileSystem: self.fileSystem) - return execInfos.filter{!$0.supportedTriples.isEmpty} + private func parseExecutableArtifactsArchive(for module: BinaryModule, triple: Triple) throws -> [ExecutableInfo] { + try self.externalExecutablesCache.memoize(key: module) { + let execInfos = try module.parseExecutableArtifactArchives(for: triple, fileSystem: self.fileSystem) + return execInfos.filter { !$0.supportedTriples.isEmpty } + } + } + + func parseLibraryArtifactsArchive(for module: BinaryModule, triple: Triple) throws -> [LibraryInfo] { + try self.externalLibrariesCache.memoize(key: module) { + try module.parseLibraryArtifactArchives(for: triple, fileSystem: self.fileSystem) } } } diff --git a/Sources/Build/BuildPlan/BuildPlan+Swift.swift b/Sources/Build/BuildPlan/BuildPlan+Swift.swift index acdca547553..9e16e02f0e0 100644 --- a/Sources/Build/BuildPlan/BuildPlan+Swift.swift +++ b/Sources/Build/BuildPlan/BuildPlan+Swift.swift @@ -11,19 +11,23 @@ //===----------------------------------------------------------------------===// import struct Basics.InternalError -import class PackageModel.BinaryTarget -import class PackageModel.ClangTarget -import class PackageModel.SystemLibraryTarget + +import class PackageModel.BinaryModule +import class PackageModel.ClangModule +import class PackageModel.SystemLibraryModule + +import PackageGraph +import PackageLoading +import SPMBuildCore extension BuildPlan { - func plan(swiftTarget: SwiftTargetBuildDescription) throws { + func plan(swiftTarget: SwiftModuleBuildDescription) throws { // We need to iterate recursive dependencies because Swift compiler needs to see all the targets a target - // depends on. - let environment = swiftTarget.buildParameters.buildEnvironment - for case .target(let dependency, _) in try swiftTarget.target.recursiveDependencies(satisfying: environment) { + // builds against + for case .module(let dependency, let description) in swiftTarget.recursiveLinkDependencies(using: self) { switch dependency.underlying { - case let underlyingTarget as ClangTarget where underlyingTarget.type == .library: - guard case let .clang(target)? = targetMap[dependency] else { + case let underlyingTarget as ClangModule where underlyingTarget.type == .library: + guard case let .clang(target)? = description else { throw InternalError("unexpected clang target \(underlyingTarget)") } // Add the path to modulemap of the dependency. Currently we require that all Clang targets have a @@ -35,11 +39,28 @@ extension BuildPlan { "-Xcc", "-fmodule-map-file=\(moduleMap.pathString)", "-Xcc", "-I", "-Xcc", target.clangTarget.includeDir.pathString, ] - case let target as SystemLibraryTarget: + case let target as SystemLibraryModule: swiftTarget.additionalFlags += ["-Xcc", "-fmodule-map-file=\(target.moduleMapPath.pathString)"] swiftTarget.additionalFlags += try pkgConfig(for: target).cFlags - case let target as BinaryTarget: - if case .xcframework = target.kind { + case let target as BinaryModule: + switch target.kind { + case .unknown: + break + case .artifactsArchive: + let libraries = try self.parseLibraryArtifactsArchive(for: target, triple: swiftTarget.buildParameters.triple) + for library in libraries { + library.headersPaths.forEach { + swiftTarget.additionalFlags += ["-I", $0.pathString, "-Xcc", "-I", "-Xcc", $0.pathString] + } + if let moduleMapPath = library.moduleMapPath { + // We need to pass the module map if there is one. If there is none Swift cannot import it but + // this might still be valid + swiftTarget.additionalFlags += ["-Xcc", "-fmodule-map-file=\(moduleMapPath)"] + } + + swiftTarget.libraryBinaryPaths.insert(library.libraryPath) + } + case .xcframework: let libraries = try self.parseXCFramework(for: target, triple: swiftTarget.buildParameters.triple) for library in libraries { library.headersPaths.forEach { @@ -53,5 +74,4 @@ extension BuildPlan { } } } - } diff --git a/Sources/Build/BuildPlan/BuildPlan+Test.swift b/Sources/Build/BuildPlan/BuildPlan+Test.swift index 93fd9da29c7..545ecb2702f 100644 --- a/Sources/Build/BuildPlan/BuildPlan+Test.swift +++ b/Sources/Build/BuildPlan/BuildPlan+Test.swift @@ -15,161 +15,200 @@ import struct Basics.InternalError import struct Basics.AbsolutePath import struct LLBuildManifest.TestDiscoveryTool import struct LLBuildManifest.TestEntryPointTool -import struct PackageGraph.PackageGraph +import struct PackageGraph.ModulesGraph + +@_spi(SwiftPMInternal) +import struct PackageGraph.ResolvedPackage + +@_spi(SwiftPMInternal) import struct PackageGraph.ResolvedProduct -import struct PackageGraph.ResolvedTarget + +@_spi(SwiftPMInternal) +import struct PackageGraph.ResolvedModule + import struct PackageModel.Sources -import class PackageModel.SwiftTarget -import class PackageModel.Target +import enum PackageModel.BuildSettings +import class PackageModel.SwiftModule +import class PackageModel.Module import struct SPMBuildCore.BuildParameters import protocol TSCBasic.FileSystem extension BuildPlan { static func makeDerivedTestTargets( - _ buildParameters: BuildParameters, - _ graph: PackageGraph, - _ disableSandbox: Bool, + testProducts: [ProductBuildDescription], + destinationBuildParameters: BuildParameters, + toolsBuildParameters: BuildParameters, + shouldDisableSandbox: Bool, _ fileSystem: FileSystem, _ observabilityScope: ObservabilityScope - ) throws -> [(product: ResolvedProduct, discoveryTargetBuildDescription: SwiftTargetBuildDescription?, entryPointTargetBuildDescription: SwiftTargetBuildDescription)] { - guard buildParameters.testingParameters.testProductStyle.requiresAdditionalDerivedTestTargets, - case .entryPointExecutable(let explicitlyEnabledDiscovery, let explicitlySpecifiedPath) = - buildParameters.testingParameters.testProductStyle - else { - throw InternalError("makeTestManifestTargets should not be used for build plan which does not require additional derived test targets") + ) throws -> [(product: ResolvedProduct, discoveryTargetBuildDescription: SwiftModuleBuildDescription?, entryPointTargetBuildDescription: SwiftModuleBuildDescription)] { + var explicitlyEnabledDiscovery = false + var explicitlySpecifiedPath: AbsolutePath? + if case let .entryPointExecutable(caseExplicitlyEnabledDiscovery, caseExplicitlySpecifiedPath) = destinationBuildParameters.testProductStyle { + explicitlyEnabledDiscovery = caseExplicitlyEnabledDiscovery + explicitlySpecifiedPath = caseExplicitlySpecifiedPath } - let isEntryPointPathSpecifiedExplicitly = explicitlySpecifiedPath != nil var isDiscoveryEnabledRedundantly = explicitlyEnabledDiscovery && !isEntryPointPathSpecifiedExplicitly - var result: [(ResolvedProduct, SwiftTargetBuildDescription?, SwiftTargetBuildDescription)] = [] - for testProduct in graph.allProducts where testProduct.type == .test { - guard let package = graph.package(for: testProduct) else { - throw InternalError("package not found for \(testProduct)") - } - isDiscoveryEnabledRedundantly = isDiscoveryEnabledRedundantly && nil == testProduct.testEntryPointTarget + var result: [(ResolvedProduct, SwiftModuleBuildDescription?, SwiftModuleBuildDescription)] = [] + for testBuildDescription in testProducts { + let testProduct = testBuildDescription.product + let package = testBuildDescription.package + + isDiscoveryEnabledRedundantly = isDiscoveryEnabledRedundantly && nil == testProduct.testEntryPointModule // If a non-explicitly specified test entry point file exists, prefer that over test discovery. // This is designed as an escape hatch when test discovery is not appropriate and for backwards // compatibility for projects that have existing test entry point files (e.g. XCTMain.swift, LinuxMain.swift). - let toolsVersion = graph.package(for: testProduct)?.manifest.toolsVersion ?? .v5_5 + let toolsVersion = package.manifest.toolsVersion // If `testProduct.testEntryPointTarget` is non-nil, it may either represent an `XCTMain.swift` (formerly `LinuxMain.swift`) file // if such a file is located in the package, or it may represent a test entry point file at a path specified by the option // `--experimental-test-entry-point-path `. The latter is useful because it still performs test discovery and places the discovered // tests into a separate target/module named "PackageDiscoveredTests". Then, that entry point file may import that module and // obtain that list to pass it to the `XCTMain(...)` function and avoid needing to maintain a list of tests itself. - if testProduct.testEntryPointTarget != nil && explicitlyEnabledDiscovery && !isEntryPointPathSpecifiedExplicitly { - let testEntryPointName = testProduct.underlying.testEntryPointPath?.basename ?? SwiftTarget.defaultTestEntryPointName + if testProduct.testEntryPointModule != nil && explicitlyEnabledDiscovery && !isEntryPointPathSpecifiedExplicitly { + let testEntryPointName = testProduct.underlying.testEntryPointPath?.basename ?? SwiftModule.defaultTestEntryPointName observabilityScope.emit(warning: "'--enable-test-discovery' was specified so the '\(testEntryPointName)' entry point file for '\(testProduct.name)' will be ignored and an entry point will be generated automatically. To use test discovery with a custom entry point file, pass '--experimental-test-entry-point-path '.") - } else if testProduct.testEntryPointTarget == nil, let testEntryPointPath = explicitlySpecifiedPath, !fileSystem.exists(testEntryPointPath) { + } else if testProduct.testEntryPointModule == nil, let testEntryPointPath = explicitlySpecifiedPath, !fileSystem.exists(testEntryPointPath) { observabilityScope.emit(error: "'--experimental-test-entry-point-path' was specified but the file '\(testEntryPointPath)' could not be found.") } - /// Generates test discovery targets, which contain derived sources listing the discovered tests. - func generateDiscoveryTargets() throws -> (target: SwiftTarget, resolved: ResolvedTarget, buildDescription: SwiftTargetBuildDescription) { + /// Generates test discovery modules, which contain derived sources listing the discovered tests. + func generateDiscoveryTargets() throws -> (target: SwiftModule, resolved: ResolvedModule, buildDescription: SwiftModuleBuildDescription) { let discoveryTargetName = "\(package.manifest.displayName)PackageDiscoveredTests" - let discoveryDerivedDir = buildParameters.buildPath.appending(components: "\(discoveryTargetName).derived") + let discoveryDerivedDir = destinationBuildParameters.buildPath.appending(components: "\(discoveryTargetName).derived") let discoveryMainFile = discoveryDerivedDir.appending(component: TestDiscoveryTool.mainFileName) var discoveryPaths: [AbsolutePath] = [] + var discoveryBuildSettings: BuildSettings.AssignmentTable = .init() discoveryPaths.append(discoveryMainFile) - for testTarget in testProduct.targets { + for testTarget in testProduct.modules { let path = discoveryDerivedDir.appending(components: testTarget.name + ".swift") discoveryPaths.append(path) + // Add in the include path from the test targets to ensure this module builds + if let flags = testTarget.underlying.buildSettings.assignments[.OTHER_SWIFT_FLAGS] { + for assignment in flags { + let values = assignment.values.filter({ $0.hasPrefix("-I") }) + if !values.isEmpty { + discoveryBuildSettings.add(.init(values: values, conditions: []), for: .OTHER_SWIFT_FLAGS) + } + } + } } - let discoveryTarget = SwiftTarget( + let discoveryTarget = SwiftModule( name: discoveryTargetName, - dependencies: testProduct.underlying.targets.map { .target($0, conditions: []) }, + dependencies: testProduct.underlying.modules.map { .module($0, conditions: []) }, packageAccess: true, // test target is allowed access to package decls by default - testDiscoverySrc: Sources(paths: discoveryPaths, root: discoveryDerivedDir) + testDiscoverySrc: Sources(paths: discoveryPaths, root: discoveryDerivedDir), + buildSettings: discoveryBuildSettings, + implicit: true ) - let discoveryResolvedTarget = ResolvedTarget( + let discoveryResolvedModule = ResolvedModule( packageIdentity: testProduct.packageIdentity, underlying: discoveryTarget, - dependencies: testProduct.targets.map { .target($0, conditions: []) }, + dependencies: testProduct.modules.map { .module($0, conditions: []) }, defaultLocalization: testProduct.defaultLocalization, supportedPlatforms: testProduct.supportedPlatforms, platformVersionProvider: testProduct.platformVersionProvider ) - let discoveryTargetBuildDescription = try SwiftTargetBuildDescription( + + let discoveryTargetBuildDescription = try SwiftModuleBuildDescription( package: package, - target: discoveryResolvedTarget, + target: discoveryResolvedModule, toolsVersion: toolsVersion, - buildParameters: buildParameters, + buildParameters: testBuildDescription.buildParameters, + macroBuildParameters: toolsBuildParameters, testTargetRole: .discovery, - disableSandbox: disableSandbox, + shouldDisableSandbox: shouldDisableSandbox, fileSystem: fileSystem, observabilityScope: observabilityScope ) - return (discoveryTarget, discoveryResolvedTarget, discoveryTargetBuildDescription) + return (discoveryTarget, discoveryResolvedModule, discoveryTargetBuildDescription) } /// Generates a synthesized test entry point target, consisting of a single "main" file which calls the test entry /// point API and leverages the test discovery target to reference which tests to run. func generateSynthesizedEntryPointTarget( - swiftTargetDependencies: [Target.Dependency], - resolvedTargetDependencies: [ResolvedTarget.Dependency] - ) throws -> SwiftTargetBuildDescription { - let entryPointDerivedDir = buildParameters.buildPath.appending(components: "\(testProduct.name).derived") - let entryPointMainFileName = TestEntryPointTool.mainFileName(for: buildParameters.testingParameters.library) + swiftTargetDependencies: [Module.Dependency], + resolvedTargetDependencies: [ResolvedModule.Dependency] + ) throws -> SwiftModuleBuildDescription { + let entryPointDerivedDir = destinationBuildParameters.buildPath.appending(components: "\(testProduct.name).derived") + let entryPointMainFileName = TestEntryPointTool.mainFileName let entryPointMainFile = entryPointDerivedDir.appending(component: entryPointMainFileName) let entryPointSources = Sources(paths: [entryPointMainFile], root: entryPointDerivedDir) - let entryPointTarget = SwiftTarget( + var entryPointBuildSettings: BuildSettings.AssignmentTable = .init() + for testTarget in testProduct.modules { + // Add in the include path from the test targets to ensure this module builds + if let flags = testTarget.underlying.buildSettings.assignments[.OTHER_SWIFT_FLAGS] { + for assignment in flags { + let values = assignment.values.filter({ $0.hasPrefix("-I") }) + if !values.isEmpty { + entryPointBuildSettings.add(.init(values: values, conditions: []), for: .OTHER_SWIFT_FLAGS) + } + } + } + } + + let entryPointTarget = SwiftModule( name: testProduct.name, type: .library, - dependencies: testProduct.underlying.targets.map { .target($0, conditions: []) } + swiftTargetDependencies, + dependencies: testProduct.underlying.modules.map { .module($0, conditions: []) } + swiftTargetDependencies, packageAccess: true, // test target is allowed access to package decls - testEntryPointSources: entryPointSources + testEntryPointSources: entryPointSources, + buildSettings: entryPointBuildSettings ) - let entryPointResolvedTarget = ResolvedTarget( + + let entryPointResolvedTarget = ResolvedModule( packageIdentity: testProduct.packageIdentity, underlying: entryPointTarget, - dependencies: testProduct.targets.map { .target($0, conditions: []) } + resolvedTargetDependencies, + dependencies: testProduct.modules.map { .module($0, conditions: []) } + resolvedTargetDependencies, defaultLocalization: testProduct.defaultLocalization, supportedPlatforms: testProduct.supportedPlatforms, platformVersionProvider: testProduct.platformVersionProvider ) - return try SwiftTargetBuildDescription( + + return try SwiftModuleBuildDescription( package: package, target: entryPointResolvedTarget, toolsVersion: toolsVersion, - buildParameters: buildParameters, + buildParameters: testBuildDescription.buildParameters, + macroBuildParameters: toolsBuildParameters, testTargetRole: .entryPoint(isSynthesized: true), - disableSandbox: disableSandbox, + shouldDisableSandbox: shouldDisableSandbox, fileSystem: fileSystem, observabilityScope: observabilityScope ) } - let discoveryTargets: (target: SwiftTarget, resolved: ResolvedTarget, buildDescription: SwiftTargetBuildDescription)? - let swiftTargetDependencies: [Target.Dependency] - let resolvedTargetDependencies: [ResolvedTarget.Dependency] + let discoveryTargets: (target: SwiftModule, resolved: ResolvedModule, buildDescription: SwiftModuleBuildDescription)? + let swiftTargetDependencies: [Module.Dependency] + let resolvedTargetDependencies: [ResolvedModule.Dependency] - switch buildParameters.testingParameters.library { - case .xctest: - discoveryTargets = try generateDiscoveryTargets() - swiftTargetDependencies = [.target(discoveryTargets!.target, conditions: [])] - resolvedTargetDependencies = [.target(discoveryTargets!.resolved, conditions: [])] - case .swiftTesting: + if destinationBuildParameters.triple.isDarwin() { discoveryTargets = nil - swiftTargetDependencies = testProduct.targets.map { .target($0.underlying, conditions: []) } - resolvedTargetDependencies = testProduct.targets.map { .target($0, conditions: []) } + swiftTargetDependencies = [] + resolvedTargetDependencies = [] + } else { + discoveryTargets = try generateDiscoveryTargets() + swiftTargetDependencies = [.module(discoveryTargets!.target, conditions: [])] + resolvedTargetDependencies = [.module(discoveryTargets!.resolved, conditions: [])] } - if let entryPointResolvedTarget = testProduct.testEntryPointTarget { + if !destinationBuildParameters.triple.isDarwin(), let entryPointResolvedTarget = testProduct.testEntryPointModule { if isEntryPointPathSpecifiedExplicitly || explicitlyEnabledDiscovery { if isEntryPointPathSpecifiedExplicitly { - // Allow using the explicitly-specified test entry point target, but still perform test discovery and thus declare a dependency on the discovery targets. - let entryPointTarget = SwiftTarget( + // Allow using the explicitly-specified test entry point target, but still perform test discovery and thus declare a dependency on the discovery modules. + let entryPointTarget = SwiftModule( name: entryPointResolvedTarget.underlying.name, dependencies: entryPointResolvedTarget.underlying.dependencies + swiftTargetDependencies, packageAccess: entryPointResolvedTarget.packageAccess, testEntryPointSources: entryPointResolvedTarget.underlying.sources ) - let entryPointResolvedTarget = ResolvedTarget( + let entryPointResolvedTarget = ResolvedModule( packageIdentity: testProduct.packageIdentity, underlying: entryPointTarget, dependencies: entryPointResolvedTarget.dependencies + resolvedTargetDependencies, @@ -177,13 +216,14 @@ extension BuildPlan { supportedPlatforms: testProduct.supportedPlatforms, platformVersionProvider: testProduct.platformVersionProvider ) - let entryPointTargetBuildDescription = try SwiftTargetBuildDescription( + let entryPointTargetBuildDescription = try SwiftModuleBuildDescription( package: package, target: entryPointResolvedTarget, toolsVersion: toolsVersion, - buildParameters: buildParameters, + buildParameters: destinationBuildParameters, + macroBuildParameters: toolsBuildParameters, testTargetRole: .entryPoint(isSynthesized: false), - disableSandbox: disableSandbox, + shouldDisableSandbox: shouldDisableSandbox, fileSystem: fileSystem, observabilityScope: observabilityScope ) @@ -199,13 +239,14 @@ extension BuildPlan { } } else { // Use the test entry point as-is, without performing test discovery. - let entryPointTargetBuildDescription = try SwiftTargetBuildDescription( + let entryPointTargetBuildDescription = try SwiftModuleBuildDescription( package: package, target: entryPointResolvedTarget, toolsVersion: toolsVersion, - buildParameters: buildParameters, + buildParameters: destinationBuildParameters, + macroBuildParameters: toolsBuildParameters, testTargetRole: .entryPoint(isSynthesized: false), - disableSandbox: disableSandbox, + shouldDisableSandbox: shouldDisableSandbox, fileSystem: fileSystem, observabilityScope: observabilityScope ) @@ -229,14 +270,15 @@ extension BuildPlan { } } -private extension PackageModel.SwiftTarget { +private extension PackageModel.SwiftModule { /// Initialize a SwiftTarget representing a test entry point. convenience init( name: String, - type: PackageModel.Target.Kind? = nil, - dependencies: [PackageModel.Target.Dependency], + type: PackageModel.Module.Kind? = nil, + dependencies: [PackageModel.Module.Dependency], packageAccess: Bool, - testEntryPointSources sources: Sources + testEntryPointSources sources: Sources, + buildSettings: BuildSettings.AssignmentTable = .init() ) { self.init( name: name, @@ -245,8 +287,9 @@ private extension PackageModel.SwiftTarget { sources: sources, dependencies: dependencies, packageAccess: packageAccess, - swiftVersion: .v5, - usesUnsafeFlags: false + buildSettings: buildSettings, + usesUnsafeFlags: false, + implicit: true ) } } diff --git a/Sources/Build/BuildPlan/BuildPlan.swift b/Sources/Build/BuildPlan/BuildPlan.swift index 3689501dd01..b54f7b939c6 100644 --- a/Sources/Build/BuildPlan/BuildPlan.swift +++ b/Sources/Build/BuildPlan/BuildPlan.swift @@ -10,6 +10,7 @@ // //===----------------------------------------------------------------------===// +import _Concurrency import Basics import Foundation import LLBuildManifest @@ -18,6 +19,7 @@ import PackageGraph import PackageLoading import PackageModel import SPMBuildCore +import TSCBasic #if USE_IMPL_ONLY_IMPORTS @_implementationOnly import SwiftDriver @@ -25,77 +27,22 @@ import SPMBuildCore import SwiftDriver #endif -import enum TSCBasic.ProcessEnv - import enum TSCUtility.Diagnostics import var TSCUtility.verbosity extension String { var asSwiftStringLiteralConstant: String { - return unicodeScalars.reduce("", { $0 + $1.escaped(asASCII: false) }) - } -} - -extension Array where Element == String { - /// Converts a set of C compiler flags into an equivalent set to be - /// indirected through the Swift compiler instead. - func asSwiftcCCompilerFlags() -> Self { - self.flatMap { ["-Xcc", $0] } - } - - /// Converts a set of C++ compiler flags into an equivalent set to be - /// indirected through the Swift compiler instead. - func asSwiftcCXXCompilerFlags() -> Self { - _ = self.flatMap { ["-Xcxx", $0] } - // TODO: Pass -Xcxx flags to swiftc (#6491) - // Remove fatal error when downstream support arrives. - fatalError("swiftc does support -Xcxx flags yet.") - } - - /// Converts a set of linker flags into an equivalent set to be indirected - /// through the Swift compiler instead. - /// - /// Some arguments can be passed directly to the Swift compiler. We omit - /// prefixing these arguments (in both the "-option value" and - /// "-option[=]value" forms) with "-Xlinker". All other arguments are - /// prefixed with "-Xlinker". - func asSwiftcLinkerFlags() -> Self { - // Arguments that can be passed directly to the Swift compiler and - // doesn't require -Xlinker prefix. - // - // We do this to avoid sending flags like linker search path at the end - // of the search list. - let directSwiftLinkerArgs = ["-L"] - - var flags: [String] = [] - var it = self.makeIterator() - while let flag = it.next() { - if directSwiftLinkerArgs.contains(flag) { - // `