[Pre-Pitch] Add MacPorts Package Manager support to SwiftPM

Introduction

HomeBrew is a popular package manager solution on macOS, but some people still prefer to use MacPorts for package management, and SwiftPM should also add support for it.

Motivation

There are people keeps submitting feedback that they need support of MacPorts in SwiftPM both on the GitHub issue of SwiftPM and the Swift Forums.

See

Proposed solution

Add a new systemPackageProvider type almost the same as we do for HomeBrew, apt-get, yum and nuget.

public enum SystemPackageProvider {
    /// Packages installable by the MacPorts package manager.
    @available(_PackageDescription, introduced: 999.0)
    case portItem([String])
    
    /// Creates a system package provider with a list of installable packages
    /// for people who use the MacPorts package manager on macOS.
    ///
    /// - Parameter packages: The list of package names.
    ///
    /// - Returns: A package provider.
    public static func port(_ packages: [String]) -> SystemPackageProvider {
        return .portItem(packages)
    }
}

See the full implementation at Add MacPorts package manager support by Kyle-Ye · Pull Request #6756 · apple/swift-package-manager · GitHub

Detailed Design

For homebrew provider, we simply use $(brew --prefix)/lib/pkgconfig for a search path at first.

"$(brew --prefix)" will be evaluated to be /opt/homebrew on Apple Silicon Macs and /usr/local on Intel Macs.

MacPorts will always be /opt/local/ by default

But since Homebrew can have multiple versions of the same package, we then use a more accurate path - /usr/local/opt/(NAME)/lib/pkgconfig in SwiftPM.

I don't know if MacPorts supports similar features. For now I'll simply use the /opt/local/lib/pkgconfig first and we can improve it later.

case .brew(let packages):
    let brewPrefix: String
    if let brewPrefixOverride {
        brewPrefix = brewPrefixOverride.pathString
    } else {
        // Homebrew can have multiple versions of the same package. The
        // user can choose another version than the latest by running
        // ``brew switch NAME VERSION``, so we shouldn't assume to link
        // to the latest version. Instead use the version as symlinked
        // in /usr/local/opt/(NAME)/lib/pkgconfig.
        struct Static {
            static let value = { try? TSCBasic.Process.checkNonZeroExit(args: "brew", "--prefix").spm_chomp() }()
        }
        if let value = Static.value {
            brewPrefix = value
        } else {
            return []
        }
    }
    return try packages.map({ try AbsolutePath(validating: brewPrefix).appending(components: "opt", $0, "lib", "pkgconfig") })
case .port:
    return [try AbsolutePath(validating: "/opt/local/lib/pkgconfig")]

Impact on existing packages

Since the new API are entirely additive, there's no impact on existing packages.

And if you'd like to choose MacPorts as your provider instead of HomeBrew, simply replace it with .port.

.systemLibrary(
    name: "Clibxml",
    pkgConfig: "libxml-2.0",
    providers: [
-       .brew(["libxml2"]),
+       .port(["libxml2"]),
    ]
)

Currently we can't make a package supporting them on macOS at the same time. See Future Directions for more information.

Alternatives considered

Asking people to switch from MacPorts to HomeBrew

Asking people to switch from MacPorts to HomeBrew on macOS platform.

Using other name instead of port

@Max_Desiatov is suggesting we use macPorts instead of port across the PR.

On the macPorts side:

  1. potential conflict with other package manager named port on BSD systems.

Sticking to the name of a CLI command, e.g. brew or apt there was a mistake. IMO we should introduce cleaner names and deprecate old names based on short CLI command names. In fact, the type is named SystemPackageProvider , not SystemPackageCommand . There are enough package manager CLI commands named port or pkg on BSD systems for example. By squatting port here we'll create a mess for possible future BSD support. I'm not sure how exactly limited means of namespacing in Swift would help.

  1. port is a ambiguous in the context

Additionally, using plain port creates confusion for a reader. I'm skimming through a package and see .port there without any disambiguation. Is that a Mac port, FreeBSD port, or OpenBSD port? Brevity and consistency is good, but not at the cost of reduced readability and increased chance for confusion.

On the port side:

  1. We should use a consist API for SystemPackageProvider
  • For HomeBrew package manager (brew install xx), we use brew here.
  • For apt-get package manager (apt install xx), we use apt here.
  • So for MacPorts package manager (port install xx), we should use port here.
  1. Swift have a namespace system.
  • It is expected we only use it in the right place. We can only use .port(["xx"]) if the callee accepts a SystemPackageProvider params.

Future Directions

The current implementation of pkgConfigArgs will only pick the first provider available for the platform.

It means we can't set more than 1 provider on a given platform.

For example, both provider A and provider B are available for platform P. If we put provider A in front of provider B, then in fact, provider B will be useless on this platform. If A can provide it, it will be successful, otherwise SwiftPM will tell you to use A to install the corresponding package, and B will not be given a chance to try.

We can fix it in this PR or leave it to a future one.

5 Likes

Kudos for the pitch! I am using MacPorts since it is much more sane than Brew when it comes to the handling of different versions of the same software. Adding MacPorts should be a no-brainer.

Regards,

Lars

2 Likes

Update:
Add @Max_Desiatov 's suggestion to rename the new API from port to macPorts in Alternatives considered section.

If you’re going with a longer name, could you just match the full name directly? macPorts, with the s seems more recognizable.

4 Likes

There is no macPort thing. It will always be macPorts as an alternative API name.

Just a typo in some place, I'll update to fix it.