馃寛
Currently on our team, our developer's workflows look something like this:
bundle exec pod installSteps 4 & 5 are normally required because:
This causes a few issues for us that it feels like might be solvable within CocoaPods.
bundle exec pod install (without --repo-update) often takes 30 seconds on our projectpod install triggers an entire recompile on our appWhile we can solve the second issue, it seems like it might be worth solving both of these for everyone in CocoaPods if possible.
We've solved this on our project by creating a script (which runs in about a second) that attempts to decide if a pod install is actually necessary. Currently for our use case it works like this:
Pods doesn't exist, installPodfile.lock and Pods/Manifest.lock differ, installPods.xcodeproj requires a regen for added or removed files. To do this we compare what's currently in the project with what's on the file system using a small ruby script and xcodeproj. This step is a lot of work since we have to duplicate a lot of CocoaPods logic (like exclude_files in our own script).I'm sure there are other cases here we are missing for different configurations, but this seems to cover our case pretty well.
So questions from this are:
bundle check) of some sort for most cases?Thanks for reading!
Yep, this is definitely something that would be nice to improve, and some of the changes required for https://github.com/CocoaPods/CocoaPods/issues/6310 would help towards this I think.
The best way to handle this would be some form of pod check a la bundler, but this is difficult to get right, and would require a more detailed plan and RFC to be implemented. https://github.com/square/cocoapods-check exists, but it doesn't work too well with recent CocoaPods versions.
+1 this should be smarter. Maaaybe I can spend sometime on this but no promises at this time. :)
CocoaPods 1.3.1 did a bunch of improvements for the recompile and pod install times. They are still kinda high for large projects. I have a few improvements to upstream to make pod install even faster.
Aiming to try incremental pod install for 1.7.0
@keith you mentioned in your initial issue you had a script that attempts to diff the FS with the pods project. Any chance you can share that?
Yea, so first, to check the easy cases, we ran this script:
#!/bin/bash
if [ ! -d Pods ]; then
echo "Pod installing because no Pods directory was found"
exit 1
elif ! diff Podfile.lock Pods/Manifest.lock > /dev/null; then
echo "Pod installing because Pods have changed"
exit 1
fi
bundle exec ruby tools/check_development_pods.rb
case $? in
2)
exit 2
;;
1)
echo "Pod installing because development pods added/removed files"
exit 1
;;
*)
echo "Pods have not changed"
exit 0
;;
esac
The output of this script being 1 meant we installed (that was managed a level above this) and exiting with 0 is up to date.
The complicated part for development pods was handled by this ruby script:
require "find"
require "xcodeproj"
PROJECT_PATH = "Pods/Pods.xcodeproj"
VALID_SOURCE_EXTENSIONS = [
".m",
".swift",
]
RESOURCE_EXTENSIONS = [
".bin",
".lproj",
".storyboard",
".xcassets",
".xib",
]
RESOURCE_TARGETS = [
"Foo-FooResources",
]
DEVELOPMENT_TARGETS = [
"Foo",
]
PATH_FOR_TARGET = {
"Foo" => "Modules",
}
EXCLUSIONS_BY_TARGET = {
"Foo-watchOS" => [
"iOSOnlyFile.swift",
]
}
def is_resource_bundle_target?(target)
RESOURCE_TARGETS.any? { |name| target.display_name == name }
end
def is_development_target?(target)
DEVELOPMENT_TARGETS.any? { |name| target.display_name.start_with?(name) }
end
def path_for_target(target, subdirectory)
target_name = target.display_name.split("-").first
root_path = PATH_FOR_TARGET[target_name]
File.join(root_path, target_name, subdirectory)
end
def local_source_files_for_target(target)
Find.find(path_for_target(target, "Sources"))
.select { |path| File.file?(path) }
.map { |file| File.basename(file) }
.select { |file| VALID_SOURCE_EXTENSIONS.include? File.extname(file) }
end
def local_header_files_for_target(target)
Find.find(path_for_target(target, "Sources"))
.select { |path| File.file?(path) }
.map { |file| File.basename(file) }
.select { |file| File.extname(file) == ".h" }
end
def local_resource_files_for_target(target)
path = path_for_target(target, "Resources")
if Dir.exist? path
Find.find(path)
.map { |file| File.basename(file) }
.select { |file| RESOURCE_EXTENSIONS.include? File.extname(file) }
else
[]
end
end
def compiled_files_for_target(target)
target
.source_build_phase
.files_references
.map(&:display_name)
end
def header_files_for_target(target)
target
.headers_build_phase
.files_references
.map(&:display_name)
.reject { |file| file.end_with?("-umbrella.h") }
end
def xcode_resource_files_for_target(target)
target
.resources_build_phase
.files_references
.map(&:display_name)
end
def changed_sources_for_target(target)
exclusions = EXCLUSIONS_BY_TARGET.fetch(target.display_name, [])
local_files = local_source_files_for_target(target) - exclusions
dummy_files = ["#{ target.display_name }-dummy.m"]
compiled_files = compiled_files_for_target(target) - exclusions - dummy_files
local_files - compiled_files \
| compiled_files - local_files
end
def changed_headers_for_target(target)
exclusions = EXCLUSIONS_BY_TARGET.fetch(target.display_name, [])
local_files = local_header_files_for_target(target) - exclusions
xcode_headers = header_files_for_target(target)
local_files - xcode_headers \
| xcode_headers - local_files
end
def changed_resources_for_target(target)
exclusions = EXCLUSIONS_BY_TARGET.fetch(target.display_name, [])
local_resources = local_resource_files_for_target(target) - exclusions
xcode_resources = xcode_resource_files_for_target(target)
local_resources - xcode_resources \
| xcode_resources - local_resources
end
def find_missing_files
Xcodeproj::Project.open(PROJECT_PATH)
.targets
.select { |target| is_development_target?(target) }
.each do |target|
if is_resource_bundle_target?(target)
changed_files = changed_resources_for_target(target)
else
changed_files = changed_sources_for_target(target) \
| changed_headers_for_target(target)
end
if changed_files.count > 0
puts "Missing #{ changed_files } in #{ target.display_name }"
exit 1
end
end
end
begin
find_missing_files
rescue => error
puts "Development pods check: #{ error }, please notify #client-productivity"
exit 2
end
This is definitely a little specific to our project. All sources for our targets are nested in a single source path, in this case Modules/Foo/Sources. And we only care about a few file extensions when we're checking the project vs what is on disk, we would've added more there if we had more. Also we're kinda duplicating the exclude_files attribute from the podspec with the EXCLUSIONS_BY_TARGET for watchOS targets. The biggest downside was separating out the target names explicitly. I'm sure you can find a better way to do a lot of this, but this worked well for a while!
Thanks for sharing! I鈥檓 very interested in building this into CocoaPods as soon as I get time to devote to CocoaPods work
@keith closing this in favor of https://github.com/CocoaPods/CocoaPods/issues/8253. We are actively working on this to be built-into cocoapods.
Would love your feedback as well!
Most helpful comment
Thanks for sharing! I鈥檓 very interested in building this into CocoaPods as soon as I get time to devote to CocoaPods work