Cocoapods: Test spec dependency symbols shipped in release builds

Created on 19 Jul 2018  Â·  10Comments  Â·  Source: CocoaPods/CocoaPods

Report

When a test spec is specified in a Podfile, it will take on the test spec's dependencies and put them into the target corresponding to the pod.
pod 'LocalPod', :path => './LocalPod', :testspecs => ['Tests']

This means that, even if the dependency is never used outside of the test specs, they are still bundled into the target, and the symbols in the unused dependency end up being shipped in production builds.

The important parts of the pod spec for "LocalPod" mentioned above is as below:

s.subspec 'Core' do |subspec|
  subspec.source_files = 'Source'
end

s.default_subspecs = 'Core'

s.subspec 'TestMocks' do |subspec|
  subspec.source_files = 'Tests/Mocks/*.swift'
end

s.test_spec 'Tests' do |test_spec|
  test_spec.source_files = 'Tests/*.swift'
  test_spec.dependency 'LocalPod/TestMocks'
end

What did you do?

  • pod install
  • archive the app (there is no team or provisioning profile attached, that will have to be resolved first)
  • unzip the output IPA (unzip app.ipa)
  • run /usr/bin/nm -Uj "/Path_to_output_ipa/Payload/TransitiveTestspecDependency.app/Frameworks/LocalPod.framework/LocalPod"

What did you expect to happen?

There shouldn't be any symbols containing MockClassForSomething. If I removed the test spec specification in the Podfile so it looks like pod 'LocalPod', :path => './LocalPod', the output is

_LocalPodVersionString
_OBJC_CLASS_$_PodsDummy_LocalPod
_OBJC_METACLASS_$_PodsDummy_LocalPod
__T08LocalPod013UsefulClassInaB0VMa
__T08LocalPod013UsefulClassInaB0VMn
__T08LocalPod013UsefulClassInaB0VN

What happened instead?

Here's an output locally on my machine:

_LocalPodVersionString
_OBJC_CLASS_$_PodsDummy_LocalPod
_OBJC_METACLASS_$_PodsDummy_LocalPod
__T08LocalPod013UsefulClassInaB0VMa
__T08LocalPod013UsefulClassInaB0VMn
__T08LocalPod013UsefulClassInaB0VN
__T08LocalPod21MockClassForSomethingCACycfC
__T08LocalPod21MockClassForSomethingCACycfc
__T08LocalPod21MockClassForSomethingCMa
__T08LocalPod21MockClassForSomethingCMm
__T08LocalPod21MockClassForSomethingCMn
__T08LocalPod21MockClassForSomethingCN
__T08LocalPod21MockClassForSomethingCfD
__T08LocalPod21MockClassForSomethingCfd

CocoaPods Environment

Stack

   CocoaPods : 1.5.3
        Ruby : ruby 2.3.1p112 (2016-04-26 revision 54768) [x86_64-darwin16]
    RubyGems : 2.5.1
        Host : Mac OS X 10.13.5 (17F77)
       Xcode : 9.4.1 (9F2000)
         Git : git version 2.15.2 (Apple Git-101.1)
Repositories : master - https://github.com/CocoaPods/Specs.git @ dce8b25a6829d611e5c06440af48874d8d589812

Podfile

target 'TransitiveTestspecDependency' do
  use_frameworks!

  pod 'LocalPod', :path => './LocalPod', :testspecs => ['Tests']

end

Project that demonstrates the issue

https://github.com/allen-zeng/TransitiveTestspecDependency

hard workaround available enhancement

Most helpful comment

Yes this can be marked as an enhancement to basically not merge all subspecs into a single target.

All 10 comments

@allen-zeng thanks for the report, this is most likely fixed with 1.6.0 but I have not yet verified.

Thanks @dnkoutso - is there an alpha/beta version I could try?

I added a Gemfile like so

source 'https://rubygems.org'

gem 'cocoapods', :git => 'https://github.com/CocoaPods/CocoaPods.git'
gem 'cocoapods-core', :git => 'https://github.com/CocoaPods/Core.git'

And then performed these steps:

  1. bundle install
  2. rm -rf Pods
  3. bundle exec pod install
  4. archive the app
  5. unzip the ipa
  6. run /usr/bin/nm -Uj "/Path_to_output_ipa/Payload/TransitiveTestspecDependency.app/Frameworks/LocalPod.framework/LocalPod"

Unfortunately the output is still the same. The mock symbols are still in the output framework.

I think then this might be because it's using subspecs, and it's all getting merged in to the one built product :/ I'd probably have to recommend not using subspecs in this case

I must concede that the use of a subspec was not my top choice. I didn't see an alternative though. I tried putting the mock inside of a testspec but it turns out that it's not possible to consume a testspec like a subspec.

I'm guessing the only option right now is to have a standalone pod purely for the mocks?

That’s what we do at the moment. It’s not ideal, but subspecs dont (yet?) work for this purpose. We’ve thought about introducing some idea of a “testing_spec”, but only very briefly.

A "testing_spec" would make the intention very clear, and it would prevent accidentally including them.

That said, I'm not entirely sure how big a problem accidental inclusions of these pods is. So maybe a much simpler solution is to use a pod spec with a proper name "SomethingSomethingMocks" so it's clear that they shouldn't be used.

Yes this can be marked as an enhancement to basically not merge all subspecs into a single target.

Yes this can be marked as an enhancement to basically not merge all subspecs into a single target.

@dnkoutso I think the fix for this could have a smaller scope than that.

I think the issue is that test_specs aren't treated the same as user-created targets in terms of scoping for dependency resolution and subspec unionization.

If @allen-zeng converted the test_spec Tests to a target in the main project and specified pod 'LocalPod/TestMocks', :path => './LocalPod' for the Tests target, then I believe CocoaPods will generate 2 targets for LocalPod, one for use in the original TransitiveTestspecDependency target, and another for use in the new Test target. Consider:

# LocalPod/LocalPod.podspec

s.subspec 'Core' do |subspec|
  subspec.source_files = 'Source'
end

s.default_subspecs = 'Core'

s.subspec 'TestMocks' do |subspec|
  subspec.source_files = 'Tests/Mocks/*.swift'
end
# Podfile

use_frameworks!

target 'TransitiveTestspecDependency' do
  pod 'LocalPod', :path => './LocalPod'
end

target 'Tests' do
  pod 'LocalPod/TestMocks', :path => './LocalPod'
end

This is further illustrated by keeping the test_spec but moving the dependency :testspecs => ['Tests'] to another target in the Podfile:

# LocalPod/LocalPod.podspec

s.subspec 'Core' do |subspec|
  subspec.source_files = 'Source'
end

s.default_subspecs = 'Core'

s.subspec 'TestMocks' do |subspec|
  subspec.source_files = 'Tests/Mocks/*.swift'
end

s.test_spec 'Tests' do |test_spec|
  test_spec.source_files = 'Tests/*.swift'
  test_spec.dependency 'LocalPod/TestMocks'
end
# Podfile

use_frameworks!

target 'TransitiveTestspecDependency' do
  pod 'LocalPod', :path => './LocalPod'
end

target 'AnotherTarget' do
  pod 'LocalPod', :path => './LocalPod', :testspecs => ['Tests']
end

Here the 2 user targets create different dependency scopes, so you have TransitiveTestspecDependency relying on a version of LocalPod that includes only the Core subspec, while AnotherTarget links to the same LocalPod target that the Tests test_spec links to, which includes both Core and TestMocks subspecs.

This implies that subspecs are unioned within a scope, where scope is being defined as a user target (among other things I'm sure), while a test_spec target doesn't count as a user target for the purpose of scoping for this unionization.

I believe that if test_specs created a separate scope in the same way as user targets that @allen-zeng's issue would be solved.

This also implies a workaround, which is to not include :testspecs => ['Tests'] in the same target that you plan on distributing, but to add it to a different user target within the Podfile.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

marzapower picture marzapower  Â·  3Comments

lzwjava picture lzwjava  Â·  3Comments

Mingmingmew picture Mingmingmew  Â·  3Comments

soleares picture soleares  Â·  3Comments

pronebird picture pronebird  Â·  3Comments