Marathon
Marathon copied to clipboard
Add support for pinning specific versions of packages
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.
@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 😅)
This looks very good to me.
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
?
Great feedback 👍
-
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). AMarathonfile
is different from aPodfile
in that it's not required for running scripts, it's just a way to specify dependencies for other people running your script. -
Yeah, it's all manually done. It can either be done through the
pin
command or by adding the version annotations to either aMarathonfile
or when specifying dependencies inline (next to theimport
statement). -
That's a good idea 👍 We could add a
--version
argument toadd
, so that a package can be added and pinned directly, likemarathon add https://github.com/johnsundell/unbox --version 2.2
. -
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 🙂
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.
Hi @JohnSundell,
Do you have any updates on this issue?
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
}
}