mirror of
https://github.com/firebase/firebase-ios-sdk
synced 2026-11-09 22:36:01 +05:45
569 lines
15 KiB
Ruby
Executable File
569 lines
15 KiB
Ruby
Executable File
#!/usr/bin/ruby
|
|
|
|
# Copyright 2018 Google
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
# Syncs Xcode project folder and target structure with the filesystem. This
|
|
# script finds all files on the filesystem that match the patterns supplied
|
|
# below and changes the project to match what it found.
|
|
#
|
|
# Run this script after adding/removing tests to keep the project in sync.
|
|
|
|
require 'pathname'
|
|
|
|
# Note that xcodeproj 1.5.8 appears to be broken
|
|
# https://github.com/CocoaPods/Xcodeproj/issues/572
|
|
gem 'xcodeproj', '!= 1.5.8'
|
|
require 'xcodeproj'
|
|
|
|
|
|
def main()
|
|
# Make all filenames relative to the project root.
|
|
Dir.chdir(File.join(File.dirname(__FILE__), '..'))
|
|
|
|
sync_firestore()
|
|
end
|
|
|
|
|
|
def sync_firestore()
|
|
project = Xcodeproj::Project.open('Firestore/Example/Firestore.xcodeproj')
|
|
|
|
# Enable warnings after opening the project to avoid the warnings in
|
|
# xcodeproj itself
|
|
$VERBOSE = true
|
|
|
|
s = Syncer.new(project, Dir.pwd)
|
|
|
|
# Files on the filesystem that should be ignored.
|
|
s.ignore_files = [
|
|
'CMakeLists.txt',
|
|
'InfoPlist.strings',
|
|
'*.plist',
|
|
]
|
|
|
|
# Folder groups in the Xcode project that contain tests.
|
|
s.test_groups = [
|
|
'Tests',
|
|
'CoreTests',
|
|
'CoreTestsProtos',
|
|
'SwiftTests',
|
|
]
|
|
|
|
s.target 'Firestore_Tests_iOS' do |t|
|
|
t.source_files = [
|
|
'Firestore/Example/Tests/**',
|
|
'Firestore/core/test/**',
|
|
'Firestore/Protos/cpp/**',
|
|
'Firestore/third_party/Immutable/Tests/**',
|
|
]
|
|
t.exclude_files = [
|
|
# needs to be in project but not in target
|
|
'Firestore/Example/Tests/Tests-Info.plist',
|
|
|
|
# These files are integration tests, handled below
|
|
'Firestore/Example/Tests/Integration/**',
|
|
]
|
|
end
|
|
|
|
s.target 'Firestore_IntegrationTests_iOS' do |t|
|
|
t.source_files = [
|
|
'Firestore/Example/Tests/Integration/**',
|
|
'Firestore/Example/Tests/Util/FSTEventAccumulator.mm',
|
|
'Firestore/Example/Tests/Util/FSTHelpers.mm',
|
|
'Firestore/Example/Tests/Util/FSTIntegrationTestCase.mm',
|
|
'Firestore/Example/Tests/Util/XCTestCase+Await.mm',
|
|
'Firestore/Example/Tests/en.lproj/InfoPlist.strings',
|
|
]
|
|
end
|
|
|
|
s.sync()
|
|
sort_project(project)
|
|
if project.dirty?
|
|
project.save()
|
|
end
|
|
end
|
|
|
|
|
|
# The definition of a test target including the target name, its source_files
|
|
# and exclude_files. A file is considered part of a target if it matches a
|
|
# pattern in source_files but does not match a pattern in exclude_files.
|
|
class TargetDef
|
|
def initialize(name)
|
|
@name = name
|
|
@source_files = []
|
|
@exclude_files = []
|
|
end
|
|
|
|
attr_accessor :name, :source_files, :exclude_files
|
|
|
|
# Returns true if the given relative_path matches this target's source_files
|
|
# but not its exclude_files.
|
|
#
|
|
# Args:
|
|
# - relative_path: a Pathname instance with a path relative to the project
|
|
# root.
|
|
def matches?(relative_path)
|
|
return matches_patterns(relative_path, @source_files) &&
|
|
!matches_patterns(relative_path, @exclude_files)
|
|
end
|
|
|
|
private
|
|
# Evaluates the relative_path against the given list of fnmatch patterns.
|
|
def matches_patterns(relative_path, patterns)
|
|
patterns.each do |pattern|
|
|
if relative_path.fnmatch?(pattern)
|
|
return true
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
end
|
|
|
|
|
|
class Syncer
|
|
def initialize(project, root_dir)
|
|
@project = project
|
|
@root_dir = Pathname.new(root_dir)
|
|
|
|
@finder = DirectoryLister.new(@root_dir)
|
|
|
|
@seen_groups = {}
|
|
|
|
@test_groups = []
|
|
@targets = []
|
|
end
|
|
|
|
# Considers the given fnmatch glob patterns to be ignored by the syncer.
|
|
# Patterns are matched both against the basename and project-relative
|
|
# qualified pathname.
|
|
def ignore_files=(patterns)
|
|
@finder.add_patterns(patterns)
|
|
end
|
|
|
|
# Names the groups within the project that serve as roots for tests within
|
|
# the project.
|
|
def test_groups=(groups)
|
|
@test_groups = []
|
|
groups.each do |group|
|
|
project_group = @project[group]
|
|
if project_group.nil?
|
|
raise "Project does not contain group #{group}"
|
|
end
|
|
@test_groups.push(@project[group])
|
|
end
|
|
end
|
|
|
|
# Starts a new target block. Creates a new TargetDef and yields it.
|
|
def target(name, &block)
|
|
t = TargetDef.new(name)
|
|
@targets.push(t)
|
|
|
|
block.call(t)
|
|
end
|
|
|
|
# Synchronizes the filesystem with the project.
|
|
#
|
|
# Generally there are three separate ways a file is referenced within a project:
|
|
#
|
|
# 1. The file must be in the global list of files, assigning it a UUID.
|
|
# 2. The file must be added to folder groups, describing where it is in the
|
|
# folder view of the Project Navigator.
|
|
# 3. The file must be added to a target descrbing how it's built.
|
|
#
|
|
# The Xcodeproj library handles (1) for us automatically if we do (2).
|
|
#
|
|
# Synchronization essentially proceeds in two steps:
|
|
#
|
|
# 1. Sync the filesystem structure with the folder group structure. This has
|
|
# the effect of bringing (1) and (2) into sync.
|
|
# 2. Sync the global list of files with the targets.
|
|
def sync()
|
|
group_differ = GroupDiffer.new(@finder)
|
|
group_diffs = group_differ.diff(@test_groups)
|
|
sync_groups(group_diffs)
|
|
|
|
@targets.each do |target_def|
|
|
sync_target(target_def)
|
|
end
|
|
end
|
|
|
|
private
|
|
def sync_groups(diff_entries)
|
|
diff_entries.each do |entry|
|
|
if !entry.in_source && entry.in_target
|
|
remove_from_project(entry.ref)
|
|
end
|
|
|
|
if entry.in_source && !entry.in_target
|
|
add_to_project(entry.path)
|
|
end
|
|
end
|
|
end
|
|
|
|
# Removes the given file reference from the project after the file is found
|
|
# missing but references to it still exist in the project.
|
|
def remove_from_project(file_ref)
|
|
group = file_ref.parents[-1]
|
|
|
|
mark_change_in_group(relative_path(group))
|
|
puts " #{basename(file_ref)} - removed"
|
|
|
|
# If the file is gone, any build phase that refers to must also remove the
|
|
# file. Without this, the project will have build file references that
|
|
# contain no actual file.
|
|
@project.native_targets.each do |target|
|
|
target.build_phases.each do |phase|
|
|
if phase.include?(file_ref)
|
|
phase.remove_file_reference(file_ref)
|
|
end
|
|
end
|
|
end
|
|
|
|
file_ref.remove_from_project
|
|
end
|
|
|
|
# Adds the given file to the project, in a path starting from the test root
|
|
# that fully prefixes the file.
|
|
def add_to_project(path)
|
|
root_group = find_test_group_containing(path)
|
|
|
|
# Find or create the group to contain the path.
|
|
dir_rel_path = path.relative_path_from(root_group.real_path).dirname
|
|
group = root_group.find_subpath(dir_rel_path.to_s, true)
|
|
|
|
mark_change_in_group(relative_path(group))
|
|
|
|
file_ref = group.new_file(path.to_s)
|
|
|
|
puts " #{basename(file_ref)} - added"
|
|
return file_ref
|
|
end
|
|
|
|
# Finds a test group whose path prefixes the given entry. Starting from the
|
|
# project root may not work since not all test directories exist within the
|
|
# example app.
|
|
def find_test_group_containing(path)
|
|
@test_groups.each do |group|
|
|
rel = path.relative_path_from(group.real_path)
|
|
next if rel.to_s.start_with?('..')
|
|
|
|
return group
|
|
end
|
|
|
|
raise "Could not find an existing test group that's a parent of #{entry.path}"
|
|
end
|
|
|
|
def mark_change_in_group(group)
|
|
path = group.to_s
|
|
if !@seen_groups.has_key?(path)
|
|
puts "#{path} ..."
|
|
@seen_groups[path] = true
|
|
end
|
|
end
|
|
|
|
SOURCES = %w{.c .cc .m .mm}
|
|
|
|
def sync_target(target_def)
|
|
target = @project.native_targets.find { |t| t.name == target_def.name }
|
|
if !target
|
|
raise "Missing target #{target_def.name}"
|
|
end
|
|
|
|
files = find_files_for_target(target_def)
|
|
sources, resources = classify_files(files)
|
|
|
|
sync_build_phase(target, target.source_build_phase, sources)
|
|
end
|
|
|
|
def classify_files(files)
|
|
sources = {}
|
|
resources = {}
|
|
|
|
files.each do |file|
|
|
path = file.real_path
|
|
ext = path.extname
|
|
if SOURCES.include?(ext)
|
|
sources[path] = file
|
|
end
|
|
end
|
|
|
|
return sources, resources
|
|
end
|
|
|
|
def sync_build_phase(target, phase, sources)
|
|
# buffer changes to the phase to avoid modifying the array we're iterating
|
|
# over.
|
|
to_remove = []
|
|
phase.files.each do |build_file|
|
|
source_path = build_file.file_ref.real_path
|
|
if sources.has_key?(source_path)
|
|
# matches spec and existing target no action taken
|
|
sources.delete(source_path)
|
|
|
|
else
|
|
# in the phase but now missing in the groups
|
|
to_remove.push(build_file)
|
|
end
|
|
end
|
|
|
|
to_remove.each do |build_file|
|
|
mark_change_in_group(target.name)
|
|
|
|
source_path = build_file.file_ref.real_path
|
|
puts " #{relative_path(source_path)} - removed"
|
|
phase.remove_build_file(build_file)
|
|
end
|
|
|
|
sources.each do |path, file_ref|
|
|
mark_change_in_group(target.name)
|
|
|
|
phase.add_file_reference(file_ref)
|
|
puts " #{relative_path(file_ref)} - added"
|
|
end
|
|
end
|
|
|
|
def find_files_for_target(target_def)
|
|
result = []
|
|
|
|
@project.files.each do |file_ref|
|
|
next if file_ref.source_tree != '<group>'
|
|
|
|
rel = relative_path(file_ref)
|
|
if target_def.matches?(rel)
|
|
result.push(file_ref)
|
|
end
|
|
end
|
|
return result
|
|
end
|
|
|
|
def normalize_to_pathname(file_ref)
|
|
if !file_ref.is_a? Pathname
|
|
if file_ref.is_a? String
|
|
file_ref = Pathname.new(file_ref)
|
|
else
|
|
file_ref = file_ref.real_path
|
|
end
|
|
end
|
|
return file_ref
|
|
end
|
|
|
|
def basename(file_ref)
|
|
return normalize_to_pathname(file_ref).basename
|
|
end
|
|
|
|
def relative_path(file_ref)
|
|
file_ref = normalize_to_pathname(file_ref)
|
|
return file_ref.relative_path_from(@root_dir)
|
|
end
|
|
end
|
|
|
|
|
|
def sort_project(project)
|
|
project.groups.each do |group|
|
|
sort_group(group)
|
|
end
|
|
|
|
project.targets.each do |target|
|
|
target.build_phases.each do |phase|
|
|
phase.files.sort! { |a, b|
|
|
a.file_ref.real_path.basename <=> b.file_ref.real_path.basename
|
|
}
|
|
end
|
|
end
|
|
end
|
|
|
|
|
|
def sort_group(group)
|
|
group.groups.each do |child|
|
|
sort_group(child)
|
|
end
|
|
|
|
group.children.sort! do |a, b|
|
|
# Sort groups first
|
|
if a.isa == 'PBXGroup' && b.isa != 'PBXGroup'
|
|
-1
|
|
elsif a.isa != 'PBXGroup' && b.isa == 'PBXGroup'
|
|
1
|
|
elsif a.display_name && b.display_name
|
|
File.basename(a.display_name) <=> File.basename(b.display_name)
|
|
else
|
|
0
|
|
end
|
|
end
|
|
end
|
|
|
|
|
|
# Tracks how a file is referenced: in the project file, on the filesystem,
|
|
# neither, or both.
|
|
class DiffEntry
|
|
def initialize(path)
|
|
@path = path
|
|
@in_source = false
|
|
@in_target = false
|
|
@ref = nil
|
|
end
|
|
|
|
attr_reader :path
|
|
attr_accessor :in_source, :in_target, :ref
|
|
end
|
|
|
|
|
|
# Diffs folder groups against the filesystem directories referenced by those
|
|
# folder groups.
|
|
#
|
|
# This performs the diff starting from the directories referenced by the test
|
|
# groups in the project, finding files contained within them. When comparing
|
|
# the files it finds against the project this acts on absolute paths to avoid
|
|
# problems with arbitary additional groupings in project structure that are
|
|
# standard, e.g. "Supporting Files" or "en.lproj" which either act as aliases
|
|
# for the parent or are folders that are omitted from the project view.
|
|
# Processing the diff this way allows these warts to be tolerated, even if they
|
|
# won't necessarily be recreated if an artifact is added to the filesystem.
|
|
class GroupDiffer
|
|
def initialize(dir_lister)
|
|
@dir_lister = dir_lister
|
|
|
|
@entries = {}
|
|
@dirs = {}
|
|
end
|
|
|
|
# Finds all tests on the filesystem contained within the paths of the given
|
|
# test groups and computes a list of DiffEntries describing the state of the
|
|
# files.
|
|
#
|
|
# Args:
|
|
# - groups: A list of PBXGroup objects representing folder groups within the
|
|
# project that contain tests.
|
|
#
|
|
# Returns:
|
|
# A list of DiffEntry objects, one for each test found. If the test exists on
|
|
# the filesystem, :in_source will be true. If the test exists in the project
|
|
# :in_target will be true and :ref will be set to the PBXFileReference naming
|
|
# the file.
|
|
def diff(groups) groups.each do |group| diff_project_files(group) end
|
|
|
|
return @entries.values.sort { |a, b| a.path.basename <=> b.path.basename }
|
|
end
|
|
|
|
private
|
|
# Recursively traverses all the folder groups in the Xcode project and finds
|
|
# files both on the filesystem and the group file listing.
|
|
def diff_project_files(group)
|
|
find_fs_files(group.real_path)
|
|
|
|
group.groups.each do |child|
|
|
diff_project_files(child)
|
|
end
|
|
|
|
group.files.each do |file_ref|
|
|
path = file_ref.real_path
|
|
entry = track_file(path)
|
|
entry.in_target = true
|
|
entry.ref = file_ref
|
|
|
|
if path.file?
|
|
entry.in_source = true
|
|
end
|
|
end
|
|
end
|
|
|
|
def find_fs_files(parent_path)
|
|
# Avoid re-traversing the filesystem
|
|
if @dirs.has_key?(parent_path)
|
|
return
|
|
end
|
|
@dirs[parent_path] = true
|
|
|
|
@dir_lister.entries(parent_path).each do |path|
|
|
if path.directory?
|
|
find_fs_files(path)
|
|
next
|
|
end
|
|
|
|
entry = track_file(path)
|
|
entry.in_source = true
|
|
end
|
|
end
|
|
|
|
def track_file(path)
|
|
if @entries.has_key?(path)
|
|
return @entries[path]
|
|
end
|
|
|
|
entry = DiffEntry.new(path)
|
|
@entries[path] = entry
|
|
return entry
|
|
end
|
|
end
|
|
|
|
|
|
# Finds files on the filesystem while ignoring files that have been declared to
|
|
# be ignored.
|
|
class DirectoryLister
|
|
def initialize(root_dir)
|
|
@root_dir = root_dir
|
|
@ignore_basenames = ['.', '..']
|
|
@ignore_pathnames = []
|
|
end
|
|
|
|
def add_patterns(patterns)
|
|
patterns.each do |pattern|
|
|
if File.basename(pattern) != pattern
|
|
@ignore_pathnames.push(File.join(@root_dir, pattern))
|
|
else
|
|
@ignore_basenames.push(pattern)
|
|
end
|
|
end
|
|
end
|
|
|
|
# Finds filesystem entries that are immediate children of the given Pathname,
|
|
# ignoring files that match the the global ignore_files patterns.
|
|
def entries(path)
|
|
result = []
|
|
path.entries.each do |entry|
|
|
next if ignore_basename?(entry)
|
|
|
|
file = path.join(entry)
|
|
next if ignore_pathname?(file)
|
|
|
|
result.push(file)
|
|
end
|
|
return result
|
|
end
|
|
|
|
private
|
|
def ignore_basename?(basename)
|
|
@ignore_basenames.each do |ignore|
|
|
if basename.fnmatch(ignore)
|
|
return true
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
def ignore_pathname?(file)
|
|
@ignore_pathnames.each do |ignore|
|
|
if file.fnmatch(ignore)
|
|
return true
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
end
|
|
|
|
|
|
if __FILE__ == $0
|
|
main()
|
|
end
|