Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RFC - Multiple Swift Version Support #8191

Closed
dnkoutso opened this issue Oct 15, 2018 · 26 comments

Comments

Projects
None yet
8 participants
@dnkoutso
Copy link
Contributor

commented Oct 15, 2018

Supersedes: #7134

Multiple Swift Version Support in CocoaPods

Background

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.

Podspec DSL Changes

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.

Choosing a Swift Version

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:

  • If the podspec specifies the swift_version attribute then always use this as the value of SWIFT_VERSION build setting.
  • Otherwise, CocoaPods derives the SWIFT_VERSION value based on the target that is integrating this pod.
    • If the targets integrating this pod use different 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.

Handling Build Tooling Incompatibilities

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.

Expand target_definition DSL

One 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'

Edge Cases

  • If neither the pod author specifies a set of supported Swift versions nor does the consumer specify a 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.
    • CocoaPods does not support integrating the same pod for two different Swift versions therefore if for any reason two or more user targets require a pod with a different Swift version then this is an error which is displayed to the user. This remains the same to what CocoaPods does today.
    • If for any reason the user targets that integrate a pod do not specify the 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.
  • There is a distinct possibility in which a pod author does not specify a set of supported Swift versions in their 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.
    • If none of the requirements are not exact, then error is displayed to the user.

Maintaining Backwards Compatibility

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

Linting And Validation

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:

  • The pod author specifies a list of 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.
  • The pod author specifies a list of swift_versions their pod supports but the Swift version passed using --swift-version parameter is not included in the swift_versions list.

Alternatives Considered

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.

@lilyball

This comment has been minimized.

Copy link

commented Oct 15, 2018

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".

@dnkoutso

This comment has been minimized.

Copy link
Contributor Author

commented Oct 15, 2018

@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

@lilyball

This comment has been minimized.

Copy link

commented Oct 15, 2018

This wasn't clear to me from the RFC description. I take it this is what the :singularize => true bit does?

@dnkoutso

This comment has been minimized.

Copy link
Contributor Author

commented Oct 15, 2018

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.

@dnkoutso dnkoutso changed the title RFC - Multiple Swift Version Support Proposal RFC - Multiple Swift Version Support Oct 16, 2018

@amorde

This comment has been minimized.

Copy link
Member

commented Oct 16, 2018

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.

@jshier

This comment has been minimized.

Copy link

commented Oct 16, 2018

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.

@intoxicated

This comment has been minimized.

Copy link

commented Oct 17, 2018

+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.

@dnkoutso

This comment has been minimized.

Copy link
Contributor Author

commented Oct 17, 2018

@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).

@dnkoutso

This comment has been minimized.

Copy link
Contributor Author

commented Oct 17, 2018

@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?

@bielikb

This comment has been minimized.

Copy link

commented Oct 22, 2018

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.

@lilyball

This comment has been minimized.

Copy link

commented Oct 22, 2018

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.

@dnkoutso

This comment has been minimized.

Copy link
Contributor Author

commented Nov 1, 2018

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.

@jshier

This comment has been minimized.

Copy link

commented Dec 2, 2018

@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?

@dnkoutso

This comment has been minimized.

Copy link
Contributor Author

commented Dec 2, 2018

@jshier did you also update cocoapods-core?

might also require xcodeproj gem too

@jshier

This comment has been minimized.

Copy link

commented Dec 2, 2018

I'm using Bundler, so it should've picked up any required changes from master's gemspec, no?

@dnkoutso

This comment has been minimized.

Copy link
Contributor Author

commented Dec 2, 2018

where does your Gemfile.lock point to for cocoapods-core?

@jshier

This comment has been minimized.

Copy link

commented Dec 2, 2018

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'
@Coeur

This comment has been minimized.

Copy link
Contributor

commented Feb 14, 2019

@dnkoutso, if I attempt to publish on trunk a podspec with s.swift_versions = ['3.2', '4.0', '4.2', '5.0']:

  • will it be accepted by trunk?
  • will the podspec still be compatible with CocoaPods 1.6.0, or will it require users to update CocoaPods to an unreleased version in order to use such podspec?
@Coeur

This comment has been minimized.

Copy link
Contributor

commented Feb 14, 2019

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.

@amorde

This comment has been minimized.

Copy link
Member

commented Feb 14, 2019

@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

@Coeur

This comment has been minimized.

Copy link
Contributor

commented Feb 14, 2019

@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:

First podspec

spec.version          = '1.0.0'

Second podspec

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'
@segiddins

This comment has been minimized.

Copy link
Member

commented Feb 14, 2019

It will tell you to update to a compatible version, or use a different version of the spec

@Coeur

This comment has been minimized.

Copy link
Contributor

commented Feb 15, 2019

@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.

@lilyball

This comment has been minimized.

Copy link

commented Feb 15, 2019

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.

@dnkoutso

This comment has been minimized.

Copy link
Contributor Author

commented Feb 15, 2019

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.

@Coeur

This comment has been minimized.

Copy link
Contributor

commented Feb 22, 2019

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.

mattrubin added a commit to mattrubin/OneTimePassword that referenced this issue Apr 27, 2019

Remove the .swift-version file
CocoaPods now supports specifying Swift versions in the podspec, so usage of this file by CocoaPods is deprecated.

Also, it appears this method of specifying a Swift version was never fully supported when integrating a pod into a project:
CocoaPods/CocoaPods#8191 (comment)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.