Package Manager: Executable-Only Dependencies

Executable-Only Dependencies

Introduction

Executable-Only Dependencies would allow packages to be imported only for use in the package's development environment. Dependents of a package would not inherit the package's executable-only dependencies.

Motivation

CLI tools distributed through Swift Packages, such as linters or documentation generators, are currently imported as dependencies. Although these dependencies may only exist to use as an executable during development, the dependencies are shipped with the package. End-users of a package do not need to inherit development tools from the package.

This can encourage package developers to use more developer tools without frustrating end-users with unused dependencies.

Proposed solution

Introduce a new type of dependency .executable(...), where the parameter options are the same as .package(...). This distinguishes a package for executable-only. Executable-only packages can be used via CLI with swift run, but can not be imported in code. In return, Executable-Only Dependencies are not passed on to dependents of the package.

E.g. If package A imports an executable-only linter, and if package B imports package A, package B does not inherit the linter as a dependency.

Detailed design

New types of dependencies:

extension Package.Dependency {

    /// Adds an executable-only dependency that uses the version requirement, 
    /// starting with the given minimum version, going up to the next major version.
    public static func executable(
        name: String? = nil,
        url: String,
        from version: Version
    ) -> Package.Dependency

    /// Adds a remote executable-only dependency with a given version requirement.
    public static func executable(
        name: String? = nil,
        url: String,
        _ requirement: Package.Dependency.Requirement
    ) -> Package.Dependency

    /// Adds an executable-only dependency starting with a specific minimum 
    /// version, up to but not including a specified maximum version.
    public static func executable(
        name: String? = nil,
        url: String,
        _ range: Range<Version>
    ) -> Package.Dependency

    /// Adds an executable-only dependency starting with a specific minimum 
    /// version, going up to and including a specific maximum version.
    public static func executable(
        name: String? = nil,
        url: String,
        _ range: ClosedRange<Version>
    ) -> Package.Dependency

    /// Adds an executable-only dependency to a local package on the filesystem.
    public static func executable(
        name: String? = nil,
        path: String
    ) -> Package.Dependency
}

Example use within Manifest:

dependencies: [
   .executable(url: "https://github.com/Realm/SwiftLint", from: "0.28.1"),
]

Impact on existing packages

Existing packages won't be affected because executables can still be imported as regular packages.

Alternatives considered

Currently, developers can install executables through options like Homebrew. However, this requires additional setup for package developers to distribute the executable. Also, for packages with multiple developers, executables distributed outside of Swift Package Manager may vary between environments (the versions could be different, or not installed at all).

Follow up notes

This is my first post here, and I am not sure how exactly to move forward in the proposal process.

Any help is appreciated!

- Michael

1 Like

If your manifest is marked // swift-tools-version:5.2 or higher, any executable not depended on by any product already behaves as you describe. You even get a warning about the dependency going unused (unless you hook it up as a dependency of a dummy or test target).

In SwiftPM’s master branch (corresponding to 5.4 or higher), such an executable will not even be resolved until you ask for it with swift run.

These changes were all part of (or side effects of) SE‐0226, which is finally complete.

So some declaration like you propose is actually needed, but for the reverse reason. There is currently no clear way of saying, “Please resolve this for me all the time even though I’m not using it, and don’t warn me either.”

I envision this as something like a miscellaneousDependencies property on Package itself, which would be a list of Target.Dependency instances (because you are selecting a particular product from the package, not everything the package happens to vend). It would be a catch‐all for anything you want resolved, but that doesn’t meaningfully belong to a particular target.

In addition to executables, you might use it to depend on a package that had data (such as one housing the Unicode Character Database) that you parse out what you need and distil it with a script from time to time during development to generate some sources, but where your clients don’t need the whole mammoth database.

2 Likes

When I wrote this proposal, I was using older projects that were not using 5.2, and I just updated everything and feel a little embarrassed I wrote all this without updating and checking.

I just familiarized myself with SE-0226, and in the alternatives section, they discuss why a solution like mine wouldn't be as elegant, and I agree.

A miscellaneousDependencies property could be useful. Although, when I imagine use-cases for such dependencies, the concept of an unpublished/private product might be better. (product might be the wrong term if it's private?)

E.g. a local executable product that depends on a database (like you mentioned) to generate sources. And because the executable product is unpublished/private, the dependencies wouldn't be passed on.

Either way, it seems my proposal is no longer relevant. I appreciate the discussion, it's helping me better understand the current needs of the package manager.