Supersedes: https://github.com/CocoaPods/CocoaPods/issues/7134
In CocoaPods 1.4.x, the swift_version DSL was introduced in an effort to help pod authors properly declare the version of Swift their pod works with. Additionally, Xcode recently started supporting a SWIFT_VERSION build setting per target, rather than a global setting used across all targets.
While the initial DSL served fairly well, pod authors are still willing to support multiple versions of Swift and their infrastructure often ensures their pod works with each version supported. However, today pod authors have no way of transcribing this information within their podspec file and communicate that to their consumers.
CocoaPods has also historically supported specifying only a _single_ Swift version during its lint process either via providing a .swift-version file or by passing --swift-version parameter during to lint making matters more complex and confusing since pod authors have multiple ways to specify which Swift version to use during publishing.
This proposal aims to extend the initial DSL and allow pod authors to specify a set of Swift versions supported within their podspec file, as well as further deprecate the usage of the .swift-versionfile and the --swift-version parameter used by lint.
The current DSL in CocoaPods is declared as a root attribute, in singular form and accepts a single String value.
_Before:_
# @!method swift_version=(version)
#
# The version of Swift that the specification supports.
#
# @example
#
# spec.swift_version = '3.2'
#
# @param [String] swift_version
#
root_attribute :swift_version,
:multi_platform => false
The DSL will be expanded to allow multiple Swift versions, in plural form and can accept both a String or an Array<String>.
_After:_
# @!method swift_versions=(version)
#
# The versions of Swift that the specification supports. A version of '4' will be treated as
# '4.0' by CocoaPods and not '4.1' or '4.2'.
#
# **Note** The Swift compiler mostly accepts major versions and sometimes will honor minor versions.
# While CocoaPods allows specifying a minor or patch version it might not be honored fully by the Swift compiler.
#
# @example
#
# spec.swift_versions = ['3.2']
#
# @example
#
# spec.swift_versions = ['3.2', '4.0', '4.2']
#
# @example
#
# spec.swift_version = '3.2'
#
# @example
#
# spec.swift_version = '3.2', '4.0'
#
# @param [String, Array<String>] swift_versions
#
root_attribute :swift_versions,
:container => Array,
:singularize => true
Each version of Swift specified in the podspec will be parsed as a Pod::Version class internally. For pod authors, there will be no support for version requirements (such as >=) or pre-release versions in order to prevent CocoaPods from dealing with the complexity of automatically deriving which Swift version to choose in more complex scenarios.
Even across minor Swift version updates there could be breaking compilation changes and therefore it is preferable for pod authors to explicitly specify which Swift versions (including minor ones) their pod has been tested with instead of providing unbounded requirements.
CocoaPods has historically and intentionally tried to stay away from keeping track of new Xcode releases or Swift version updates as much as possible since it is difficult to predict Apple's roadmap.
Since the introduction of the swift_version DSL CocoaPods has used the following simple strategy to pick which SWIFT_VERSION value to set during installation:
podspec specifies the swift_version attribute then _always_ use this as the value of SWIFT_VERSION build setting.SWIFT_VERSION value based on the target that is integrating this pod.SWIFT_VERSION settings then CocoaPods installation errors out with a message explaining to the user that a SWIFT_VERSION setting cannot be derived._Note_: While the pod author was initially allowed to publish their podspec using .swift-version file or --swift-version parameter, neither of them are preserved when a consumer is integrating their pod, therefore it is imperative to motivate pod authors to update their podspec to use the swift_versions DSL going forward.
Let's take the following example podspec:
Pod::Spec.new do |s|
s.name = 'CannonPodder'
s.version = '1.0.0'
# ...other required attributes here...
s.swift_versions = ['3.0', '4.0']
end
CocoaPods would still follow more or less the same strategy as described above except with this change the _latest_ (maximum) Swift version (in this case '4.0') would be the one chosen to use for the CannonPodder target.
Consumers of pods should be given options to filter and select a Swift version they would like to use for their projects based on the versions supported by each pod. Given that the majority of pod authors today have not migrated their podspec files to use the new DSL certain fallbacks must be present to ensure things continue to work.
target_definition DSLOne common example of incompatibility that can arise is when a consumer of a pod is unable to use the latest Swift version that is specified in a podspec, perhaps because their toolchain does not yet support it. Let's take the following example into consideration:
Pod::Spec.new do |s|
s.name = 'CannonPodder'
s.version = '1.0.0'
# ...other required attributes here...
s.swift_versions = ['3.0', '4.0']
end
For the sake of argument, let's also assume the consumer of the 'CannonPodder' pod are still on Xcode 8 and can only support Swift 3. Based on our described logic above, CocoaPods will always pick the latest version to compile 'CannonPodder' with, in this case '4.0', which would not work for the consumer and yield a compilation error.
To overcome this, the following DSL is proposed to be added to the target_definition class:
target 'MyApp' do
supports_swift_versions '>= 3.0', '< 4.0'
pod 'CannonPodder'
end
_Note_: This can also be declared at the root level of a Podfile in which it will be applied to all targets.
Internally, this will be stored in the hash of the target_definition instance and then later converted into a Pod::Requirement and used to compare each supported Swift version of the 'CannonPodder' pod and check whether it is satisfied by that requirement during installation.
If the consumer does not specify a supports_swift_versions declaration then the default requirement of >= 0 will be used which will essentially work as picking the _latest_ Swift version supported by the pod author.
Last but not least, if consumers would like to override the Swift version for only a specific pod then they do so via a post_install hook:
post_install do |installer|
target = installer.pods_project.targets.find do |target|
target.name == 'CannonPodder'
end
target.build_configurations.each do |config|
config.build_settings['SWIFT_VERSION'] = '3.0'
end
end
In the near future, but not as part of this proposal, this could be further enhanced as a first class API like so:
pod 'CannonPodder', '~> 1.0', :swift_version => '3.0'
supports_swift_versions entry then the Swift version of the pod is derived automatically from the user targets that integrate it. In the example above the Swift version used will be the one that 'MyApp' uses and is inspected by CocoaPods during installation by parsing the SWIFT_VERSION attribute. This remains the same to what CocoaPods does today.SWIFT_VERSION attribute then the Swift version cannot be derived at all and an error is displayed to the user. Again, this remains the same to what CocoaPods does today.podspec file, but the consumer has declared a supports_swift_versions entry. In this case the Swift version will be chosen based on the maximum exact requirement provided by the consumer.The old DSL of swift_version is in singular form which means that there are many podspec files already published using "swift_version" key within their JSON format. As part of this change, we must ensure that the old value is still parsed and accounted for in the set of Swift versions a pod supports. The new DSL will be in plural version but also provide a singularized form to avoid breaking existing podspec files written in Ruby.
This following change would need to occur in root_attribute_accessors.rb file that parses the Swift version attribute to support the old JSON format.
_Before:_
# @return [Version] The swift_versions required to use the specification.
#
def swift_version
@swift_version ||= if version = attributes_hash['swift_version']
Version.new(version)
end
end
_After:_
# @deprecated in favor of #swift_versions
#
# @return [Version] The Swift version specified by the specification.
#
def swift_version
swift_versions.last
end
# @return [Array<Version>] The Swift versions supported by the specification.
#
def swift_versions
@swift_versions ||= begin
swift_versions = Array(attributes_hash['swift_versions'])
# Pre 1.7.0, the DSL was singularized as it supported only a single version of Swift. In 1.7.0 the DSL
# is now pluralized always and a specification can support multiple versions of Swift. This ensures
# we parse the old JSON serialized format and include it as part of the Swift versions supported.
swift_versions << attributes_hash['swift_version'] unless attributes_hash['swift_version'].nil?
swift_versions.map { |swift_version| Version.new(swift_version) }.uniq.sort
end
end
Ultimately, for lint the preferred path is to completely remove support for the .swift-version file to reduce confusion, however, this would be a breaking change for the majority of pod authors and therefore until a major version update of CocoaPods that option will have to remain.
Given the above, we can still further motivate pod authors to migrate to use the swift_versions DSL by soft-deprecating the usage of .swift-version file. If the pod author is using it without declaring the swift_versions DSL then a warning should be included in the results of lint output notifying the pod author and encouraging them to switch and use the DSL instead.
It is generally difficult for pod authors to maintain infrastructure to support multiple (primarily older) versions of Swift, therefore, the validation process should not lint the pod for _all_ Swift versions declared, and instead follow the default strategy of choosing the latest (maximum) Swift version to validate with.
For pod authors who _do_ have the infrastructure (such as a CI system) that validates their pod works for older versions of Swift, the --swift-version parameter can be used to override the default behavior and ensure their code can successfully compile with a different version of Swift.
Validation will fail with an error if any of the following statements is true:
swift_versions their pod supports but a .swift-version file is present that includes a version of Swift that is _not_ included in the swift_versions list.swift_versions their pod supports but the Swift version passed using --swift-versionparameter is _not_ included in the swift_versions list.Currently the only alternative is to not do this at all and maintain the status quo, however, this will not be sustainable in the future especially given the impending launch of Swift 5 in early 2019.
I see no reason for the Podspec attribute name to change. I should be able to take a file that says
s.swift_version = '3.2'
and update that to
s.swift_version = '3.2', '4.0'
without having to change the property name. This can still serialize to JSON as described, with a singular value serializing as "swift_version" and plural value serializing as "swift_versions".
@kballard I have verified that...
s.swift_version = '3.2', '4.0'
...already works with this change.
There is a test added for it, see https://github.com/CocoaPods/Core/pull/467/files#diff-b23f0208f2c5d1e7892c8052a4ca6f15R28
This wasn't clear to me from the RFC description. I take it this is what the :singularize => true bit does?
It automatically creates the singular method of the DSL yes.
It will always serialize to JSON with the declared attribute key (in this case '"swift_versions"').
But for when we read the values (either declared using Array or String) https://github.com/CocoaPods/Core/pull/467/files#diff-118377d977381058795fda5d80be5942R45 this takes care of it.
Each version will be parsed for validity so if an author specifies garbage it will crash.
root_attribute :swift_versions,
:container => Array,
:singularize => true
CocoaPods has historically supported singularized and plural forms of attributes where appropriate, but from my experience that has lead to confusion when authoring pods for the first time.
One example is framework / frameworks - Pod authors may start with framework if they only link with 1 system framework:
s.framework = 'Foundation'
then as the library grows, they might add another framework like this:
s.framework = 'Foundation', 'AVFoundation'
and now things are confusing, because this actually works but it is not really clear why since there's another attribute called frameworks - do I need to use frameworks if I have more than one? Does framework only work if you pass it a singular String?
Given that, I'd argue for the inverse of @kballard's suggestion: opt for keeping the DSL clear and explicit by supporting only the plural form swift_versions, and deprecate the singular form swift_version. This will also make it easier to detect whether the old (current) DSL is being used or whether the new, array-based version is being used. We can then easily emit deprecation warnings if desired.
I'll once again suggest that CP should be able to more intelligently set the target Swift version based on the integrating project's version and the pod's versions, rather than just defaulting to the latest or requiring users who can't use the latest version to edit their Podfile with a supported_swift_versions setting. Otherwise there will be a (likely diminishing, as compatibility between Swift versions increases) rash of issues opened here and on library repos (something Alamofire is sensitive to, as we usually come up first alphabetically for these failures) every time there's a version update. Additionally, once users add supported_swift_versions to their Podfile, it becomes difficult for them to know when to remove it, as it covers newly supported Swift versions (unless you'll warn?).
An additional issue is the possible confusion between the support for ranges in supported_swift_versions but not spec.swift_versions, but that's relatively minor with good diagnostics.
+1 for this. With current pod system, I cannot deploy my pod which contains multiple other pods with various swift version due to error when executing pod trunk push.
@amorde Agreed overall with the intention. I don't think we want to dive into changing the DSL and its current semantics as part of this change.
We get lucky with this proposal because the singularize will not break existing clients.
Singularization has no meaningful impact on the code design and it seems its pretty well handled internally. This is only a problem due to the initial version of swift_version DSL which intentionally disallowed multiple versions (it was naive).
@jshier I'd like to hear more of your suggestion.
I'll once again suggest that CP should be able to more intelligently set the target Swift version based on the integrating project's version and the pod's versions
We can be doing some of that...but why? Shouldn't the pod author dictate the preferred version of Swift to compile their pod with?
supported_swift_versions is not meant to be removed and it can be unbounded with >= 4.0 for example.
The project based setting might not be sufficient to add version requirements although as I am typing this we could implicitly treat it as a requirement internally perhaps?
Nice proposal. Would be nice to take it one step further for pod authors that offer closed binaries compiled with different Swift versions.
e.g. I'd like to specify within pod file all supported swift versions + paths to those versions.
This sounds like a potential future enhancement to be done in the event that Apple introduces a version of the Swift compiler that is compatible with multiple Swift versions but does not allow mixing them.
At the moment, all released Swift compilers that support multiple Swift versions also support linking libraries/frameworks built with different supported Swift versions together.
I would advise against trying to design a system to handle incompatible Swift versions until we know how the compiler will handle this in the first place.
This has been merged and will ship with 1.7.0. Can use it via bundler and point it to master branch if anyone wants to try it.
@dnkoutso Trying to use this feature on master, I keep getting:
NoMethodError - undefined method `swift_versions' for #<Pod::Specification name="Alamofire">
Is this attribute (or the singular version) now required?
@jshier did you also update cocoapods-core?
might also require xcodeproj gem too
I'm using Bundler, so it should've picked up any required changes from master's gemspec, no?
where does your Gemfile.lock point to for cocoapods-core?
Ah, looks like it's pulling 1.6.0.beta.2. So I needed this in the Gemfile:
gem 'cocoapods', git: 'https://github.com/CocoaPods/CocoaPods', branch: 'master'
gem 'cocoapods-core', git: 'https://github.com/CocoaPods/Core', branch: 'master'
@dnkoutso, if I attempt to publish on trunk a podspec with s.swift_versions = ['3.2', '4.0', '4.2', '5.0']:
Seems like CocoaPods 1.6.0 would fail to accept such podspec. Maybe we should consider releasing a CocoaPods 1.6.1 version that will _ignore_ the swift_versions parameter instead of erroring out.
@Coeur you are correct, older versions of CocoaPods would not be able to accept a Podspec using DSL from the newer version.
One thing you can do is use the cocoapods_version attribute to specify that your Specification requires a certain version of CocoaPods to work properly.
Currently trunk will accept it as long as the version you are using to submit the Podspec supports the attribute
@amorde
One thing you can do is use the cocoapods_version attribute to specify that your Specification requires a certain version of CocoaPods to work properly.
OK, so just to confirm, if I publish two podspecs with:
spec.version = '1.0.0'
spec.version = '1.0.1'
spec.cocoapods_version = '>= 1.7.0'
spec.swift_versions = ['4.0', '4.2', '5.0']
And if I do pod update with CocoaPods 1.6.0, it will correctly use the first podspec? Or will it error out telling me to update to a newer version of CocoaPods?
And can I specify a beta version as the minimum version? Like:
spec.cocoapods_version = '>= 1.7.0 beta 1'
It will tell you to update to a compatible version, or use a different version of the spec
@segiddins oh... then it will be problematic during the Beta period of 1.7.0, because pods owners testing/publishing updated podspec (with swift_versions) will easily break their compatibility for the many users not ready to install a beta version of CocoaPods.
We better either have an intermediate release of CocoaPods 1.6.1 that will simply accept and ignore the parameter swift_versions, or we need to make the Beta period of 1.7.0 much shorter than the awfully long beta period that we had for 1.6.0: like a one month beta period max instead of six months.
Will they? A published podspec is just a JSON file, right? This incompatibility should only exist for the person doing the publishing, not the person consuming the podspec by listing the dependency in their Podfile.
Correct. All podspecs published to trunk are JSON. 1.7.0 _is_ backwards compatible and will be able to parse the swift_version (singular) from previously published pods as described in the initial post here.
Well, CocoaPods 1.6.1 was published a few hours ago, so now the opportunity is missed. Let's hope the beta period of 1.7.0 will be as short as possible, to minimize troubles with podspecs using swift_versions published too early.
Most helpful comment
Ah, looks like it's pulling
1.6.0.beta.2. So I needed this in theGemfile: