Marathon icon indicating copy to clipboard operation
Marathon copied to clipboard

Add support for pinning specific versions of packages

Open JohnSundell opened this issue 7 years ago • 7 comments

In order to make it easier to work with & share scripts that have dependencies, we should add support for pinning a certain version of a package to a script. The Swift Package Manager already supports this feature, but using it in Marathon requires circumventing the global package cache that Marathon uses to speed things up (to not have to re-clone and re-build each package for all scripts).

For example, the user should be able to use version 1.0 of a given package in one script, and then version 2.0 of that same package in another script, and doing this shouldn't affect the global package version.

Changes required

Version pinning in a Marathonfile

Currently a Marathonfile simply consists of newline separated URLs, so we'll need to add a syntax to define what version of a given dependency that should be used. This syntax is proposed:

https://github.com/johnsundell/unbox.git @ 2.2
https://github.com/johnsundell/[email protected]

Note that it shouldn't matter whether spaces are added around the @ character, both should parse correctly.

If the version suffix is omitted the behavior will be the same as today, the version that Marathon currently has in its package cache will be used.

Version pinning for inline dependencies

Similarly, we should allow for the same syntax to be used for inline dependency annotations, like this:

import Unbox // marathon:https://github.com/johnsundell/unbox.git @ 2.2

Pinning a version on the command line

We also need to support pinning a version to a script from the command line. The proposal here is to introduce a pin command that pins a given version of a package (that needs to already be added to Marathon) to a script at a given path, like this:

$ marathon pin Unbox 2.2 ~/Scripts/MyUnboxScript.swift

We could also allow the script name to be omitted, if the current folder only contains a single script.

Unpinning a version on the command line

Finally, we need a way to "unpin" a version of a package from a script. This only needs command line support, and could either be done by adding a --remove argument to the pin command, like this:

$ marathon pin Unbox ~/Scripts/MyUnboxScript.swift --remove

But to be more clear and consistent with our other commands, the proposal is instead to add a dedicated unpin command:

$ marathon unpin Unbox ~/Scripts/MyUnboxScript.swift

Like with the pin command, we could allow the script name to be omitted, if the current folder only contains a single script.

JohnSundell avatar May 31 '17 16:05 JohnSundell

@kareman @darthpelo @clayellis @pixyzehn @cojoj @alexaubry @garricn Would love any comments you might have on this 😄 (Let me know if it's annoying that I ping you on Marathon issues/PRs btw 😅)

JohnSundell avatar May 31 '17 16:05 JohnSundell

This looks very good to me.

kareman avatar May 31 '17 16:05 kareman

Seems like a reasonable request. Similar to version pinning in a Podfile, right? So marathon pin will just append @{VERSION} to the Marathonfile? Can the user do this manually? Can the user specify the pin when running marathon add? Should we use the pin for Package.swift for marathon export?

garricn avatar May 31 '17 17:05 garricn

Great feedback 👍

  1. No, it'll keep track of the pinned versions internally, probably as part of the package cache (each package has a Package object that specifies its version, name, url). A Marathonfile is different from a Podfile in that it's not required for running scripts, it's just a way to specify dependencies for other people running your script.

  2. Yeah, it's all manually done. It can either be done through the pin command or by adding the version annotations to either a Marathonfile or when specifying dependencies inline (next to the import statement).

  3. That's a good idea 👍 We could add a --version argument to add, so that a package can be added and pinned directly, like marathon add https://github.com/johnsundell/unbox --version 2.2.

  4. Totally! 👍 It's fine for the initial implementation of export not to support pinning though, so it isn't blocked by this - it's something we can easily add later 🙂

JohnSundell avatar May 31 '17 17:05 JohnSundell

The lack of this feature is a dealbreaker for the use of Marathon. Without pinned dependencies, any script I write using Marathon can and will randomly break in the future when the dependency updates in a non-backwards-compatible way. This is especially true any time there's a major Swift update.

Not only do we need a way to pin, but the // marathon:url comment syntax needs to support specifying that pinned version.

lilyball avatar Apr 03 '18 00:04 lilyball

Hi @JohnSundell,

Do you have any updates on this issue?

TheCoordinator avatar Aug 06 '18 10:08 TheCoordinator

this ticket needs some more help to push things along - maybe we need a rethink.

UPDATE

so pulling apart code it's apparent that there's another framework 'releases' that seems to be doing heavy lifting around getting the latest verison. https://github.com/JohnSundell/releases


    @discardableResult public func addPackage(at url: URL, throwIfAlreadyAdded: Bool = true) throws -> Package {
        let name = try nameOfPackage(at: url)

        if throwIfAlreadyAdded {
            guard (try? folder.file(named: name)) == nil else {
                throw Error.packageAlreadyAdded(name)
            }
        }

        let latestVersion = try latestMajorVersionForPackage(at: url)
        let package = Package(name: name, url: absoluteRepositoryURL(from: url), majorVersion: latestVersion)
        try save(package: package)

        try updatePackages()
        addMissingPackageFiles()

        return package
    }


    private func latestMajorVersionForPackage(at url: URL) throws -> Int {
        printer.reportProgress("Resolving latest major version for \(url.absoluteString)...")

        let releases = try perform(Releases.versions(for: url).withoutPreReleases(),
                                   orThrow: Error.failedToResolveLatestVersion(url))

        guard let latestVersion = releases.sorted().last else {
            throw Error.failedToResolveLatestVersion(url)
        }

        return latestVersion.major
    }

internal final class AddTask: Task, Executable {
    private typealias Error = AddError

    // MARK: - Executable
    func execute() throws {
        guard let identifier = arguments.first else {
            throw Error.missingIdentifier
        }

        guard let url = URL(string: identifier) else {
            throw Error.invalidURL(identifier)
        }

        let package = try packageManager.addPackage(at: url)
        printer.output("📦  \(package.name) added")
    }
}


we would just need to target an arbitrary release here releases.sorted().last

perhaps overload AddTask - arguments.first = git url from Marathonfile arguments.second -> git version number / tag / release

We have a Version class which will accommodate string -> Version https://github.com/JohnSundell/Releases/blob/master/Sources/Version.swift#L48


@discardableResult public func addPackage(at url: URL,version: Version?, throwIfAlreadyAdded: Bool = true) throws -> Package {
        let name = try nameOfPackage(at: url)

        if throwIfAlreadyAdded {
            guard (try? folder.file(named: name)) == nil else {
                throw Error.packageAlreadyAdded(name)
            }
        }


       if let version = version{
              let specificVersion  = try specificVersionForPackage(at: url,version:Version)
   let package = Package(name: name, url: absoluteRepositoryURL(from: url), majorVersion: latestVersion)
        try save(package: package)

        try updatePackages()
        addMissingPackageFiles()

        return package
       }else{
          let latestVersion = try latestMajorVersionForPackage(at: url)
        let package = Package(name: name, url: absoluteRepositoryURL(from: url), majorVersion: latestVersion)
        try save(package: package)

        try updatePackages()
        addMissingPackageFiles()

        return package
       }
   
    }

johndpope avatar Jul 25 '19 12:07 johndpope