xcode-archive-cache 0.0.6

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,37 @@
1
+ module XcodeArchiveCache
2
+ module BuildGraph
3
+ class Graph
4
+ # @return [Array<Node>] graph nodes
5
+ #
6
+ attr_reader :nodes
7
+
8
+ # @return [Xcodeproj::Project] project
9
+ #
10
+ attr_reader :project
11
+
12
+ # @return [XcodeArchiveCache::BuildSettings::Container] root target build settings
13
+ #
14
+ attr_reader :dependent_build_settings
15
+
16
+ # @param [Xcodeproj::Project] project
17
+ # @param [XcodeArchiveCache::BuildSettings::Container] dependent_build_settings
18
+ #
19
+ def initialize(project, dependent_build_settings)
20
+ @nodes = []
21
+ @project = project
22
+ @dependent_build_settings = dependent_build_settings
23
+ end
24
+
25
+ # @param [String] name
26
+ # Native target display name
27
+ #
28
+ def node_by_name(name)
29
+ nodes.select {|node| node.name == name}.first
30
+ end
31
+
32
+ def to_s
33
+ nodes.map(&:to_s).join("\n")
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,101 @@
1
+ module XcodeArchiveCache
2
+ module BuildGraph
3
+ class NativeTargetFinder
4
+
5
+ # @param [Array<Xcodeproj::Project>] projects
6
+ #
7
+ def initialize(projects)
8
+ @all_targets = projects
9
+ .map {|project| unnest(project)}
10
+ .flatten
11
+ .uniq
12
+ .map(&:native_targets)
13
+ .flatten
14
+ .select {|target| !target.test_target_type? }
15
+ end
16
+
17
+ # @param [String] platform_name
18
+ #
19
+ def set_platform_name_filter(platform_name)
20
+ @platform_name = platform_name
21
+ end
22
+
23
+ # @param [Xcodeproj::Project::Object::PBXTargetDependency] dependency
24
+ #
25
+ # @return [Xcodeproj::Project::Object::PBXNativeTarget]
26
+ #
27
+ def find_for_dependency(dependency)
28
+ # targets from embedded projects are proxied
29
+ target = dependency.target ? dependency.target : dependency.target_proxy.proxied_object
30
+ target.is_a?(Xcodeproj::Project::Object::PBXNativeTarget) ? target : nil
31
+ end
32
+
33
+ # @param [Xcodeproj::Project::Object::PBXBuildFile] file
34
+ #
35
+ # @return [Xcodeproj::Project::Object::PBXNativeTarget]
36
+ #
37
+ def find_for_file(file)
38
+ if file.file_ref.is_a?(Xcodeproj::Project::Object::PBXReferenceProxy)
39
+ project = file.file_ref.remote_ref.container_portal_object
40
+ product_reference_uuid = file.file_ref.remote_ref.remote_global_id_string
41
+ find_with_product_ref_uuid(project, product_reference_uuid)
42
+ elsif file.file_ref.is_a?(Xcodeproj::Project::Object::PBXFileReference)
43
+ # products of sibling project targets are added as PBXFileReferences
44
+ targets = find_with_product_path(file.file_ref.path)
45
+ if targets.length > 1
46
+ raise Informative, "Found more than one target with product #{File.basename(file.file_ref.path)} in:\n#{targets.map(&:project)}"
47
+ end
48
+
49
+ targets.first
50
+ end
51
+ end
52
+
53
+ # @param [String] product_name
54
+ #
55
+ def find_for_product_name(product_name)
56
+ all_targets.select {|native_target| native_target.name == product_name || native_target.product_reference.display_name == product_name}
57
+ .first
58
+ end
59
+
60
+ private
61
+
62
+ # @return [Array<Xcodeproj::Project::Object::PBXNativeTarget>]
63
+ #
64
+ attr_accessor :all_targets
65
+
66
+ # @return [String]
67
+ #
68
+ attr_accessor :platform_name
69
+
70
+ # @param [Xcodeproj::Project] project
71
+ #
72
+ # @return [Array<Xcodeproj::Project>]
73
+ #
74
+ # Project + subprojects at all levels of nesting
75
+ #
76
+ def unnest(project)
77
+ nested_projects = project.files
78
+ .select {|file_ref| File.extname(file_ref.path) == ".xcodeproj" && File.exist?(file_ref.real_path)}
79
+ .map {|file_ref| Xcodeproj::Project.open(file_ref.real_path)}
80
+ subnested_projects = nested_projects.map {|nested_project| unnest(nested_project)}.flatten
81
+ [project] + nested_projects + subnested_projects
82
+ end
83
+
84
+ # @param [String] uuid
85
+ #
86
+ # @return [Xcodeproj::Project::Object::PBXNativeTarget]
87
+ #
88
+ def find_with_product_ref_uuid(project, uuid)
89
+ project.native_targets.select {|target| target.product_reference.uuid == uuid}.first
90
+ end
91
+
92
+ # @param [String] path
93
+ #
94
+ # @return [Array<Xcodeproj::Project::Object::PBXNativeTarget>]
95
+ #
96
+ def find_with_product_path(path)
97
+ all_targets.select {|target| target.platform_name == platform_name && target.product_reference.path == path}
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,104 @@
1
+ module XcodeArchiveCache
2
+ module BuildGraph
3
+ class Node
4
+
5
+ # @return [String] native target display name
6
+ #
7
+ attr_reader :name
8
+
9
+ # @return [Boolean]
10
+ #
11
+ attr_reader :is_root
12
+
13
+ # @return [Bool] should target be rebuilt
14
+ #
15
+ attr_accessor :rebuild
16
+
17
+ # @return [String] sha256 of (input files + build settings + dependency shas)
18
+ #
19
+ attr_accessor :sha
20
+
21
+ # @return [Array<XcodeArchiveCache::BuildGraph::Node>] dependent nodes
22
+ #
23
+ attr_reader :dependent
24
+
25
+ # @return [Array<XcodeArchiveCache::BuildGraph::Node>] dependency nodes
26
+ #
27
+ attr_reader :dependencies
28
+
29
+ # @return [Xcodeproj::Project::Object::PBXNativeTarget] corresponding native target
30
+ #
31
+ attr_reader :native_target
32
+
33
+ # @return [XcodeArchiveCache::BuildSettings::Container]
34
+ #
35
+ attr_accessor :build_settings
36
+
37
+ # @param [String] name
38
+ # @param [Xcodeproj::Project::Object::PBXNativeTarget] native_target
39
+ # @param [Boolean] is_root
40
+ #
41
+ def initialize(name, native_target, is_root = false)
42
+ @name = name
43
+ @native_target = native_target
44
+ @is_root = is_root
45
+ @dependent = []
46
+ @dependencies = []
47
+ end
48
+
49
+ def has_framework_product?
50
+ native_target.product_type == Xcodeproj::Constants::PRODUCT_TYPE_UTI[:framework]
51
+ end
52
+
53
+ def has_static_library_product?
54
+ native_target.product_type == Xcodeproj::Constants::PRODUCT_TYPE_UTI[:static_library]
55
+ end
56
+
57
+ # @return [String]
58
+ #
59
+ def product_file_name
60
+ return nil unless build_settings
61
+
62
+ product_name = build_settings[XcodeArchiveCache::BuildSettings::FULL_PRODUCT_NAME_KEY]
63
+ return product_name if product_name
64
+
65
+ product_name = native_target.product_reference.name
66
+ if has_framework_product? && product_name
67
+ product_file_name = product_name
68
+ end
69
+
70
+ unless product_file_name
71
+ product_file_name = File.basename(native_target.product_reference.real_path)
72
+ end
73
+
74
+ product_file_name
75
+ end
76
+
77
+ # @return [String]
78
+ #
79
+ def dsym_file_name
80
+ return nil unless build_settings
81
+
82
+ build_settings[XcodeArchiveCache::BuildSettings::DWARF_DSYM_FILE_NAME_KEY]
83
+ end
84
+
85
+ # @return [Array<Node>]
86
+ # Direct + transitive dependents
87
+ #
88
+ def all_dependent_nodes
89
+ (dependent + dependent.map(&:all_dependent_nodes)).flatten.uniq
90
+ end
91
+
92
+ def ==(other_node)
93
+ other_node && native_target.uuid == other_node.native_target.uuid && native_target.project == other_node.native_target.project
94
+ end
95
+
96
+ def to_s
97
+ sha_string = sha ? sha : "<none>"
98
+ dependent_names = dependent.length > 0 ? dependent.map(&:name).join(", ") : "<none>"
99
+ dependency_names = dependencies.length > 0 ? dependencies.map(&:name).join(", ") : "<none>"
100
+ "#{name}\n\troot: #{is_root}\n\tproduct: #{product_file_name}\n\tsha: #{sha_string}\n\trebuild: #{rebuild}\n\tdependent: #{dependent_names}\n\tdependencies: #{dependency_names}"
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,30 @@
1
+ module XcodeArchiveCache
2
+ module BuildGraph
3
+ class RebuildEvaluator
4
+
5
+ # @param [XcodeArchiveCache::ArtifactCache::AbstractStorage] cache_storage
6
+ #
7
+ def initialize(cache_storage)
8
+ @cache_storage = cache_storage
9
+ end
10
+
11
+ # @param [XcodeArchiveCache::BuildGraph::Node] node
12
+ #
13
+ def evaluate(node)
14
+ return if node.rebuild != nil
15
+
16
+ # we include dependency shas in every node sha calculation,
17
+ # so if some dependency changes, that change propagates
18
+ # all the way to the top level
19
+ #
20
+ node.rebuild = cache_storage.cached_artifact_path(node) == nil
21
+ end
22
+
23
+ private
24
+
25
+ # @return [XcodeArchiveCache::ArtifactCache::AbstractStorage]
26
+ #
27
+ attr_reader :cache_storage
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,102 @@
1
+ module XcodeArchiveCache
2
+ module BuildGraph
3
+ class NodeShaCalculator
4
+
5
+ # @param [XcodeArchiveCache::BuildGraph::Node] node
6
+ #
7
+ def calculate(node)
8
+ return if node.sha
9
+
10
+ dependency_shas = []
11
+ node.dependencies.each do |dependency_node|
12
+ calculate(dependency_node)
13
+ dependency_shas.push(dependency_node.sha)
14
+ end
15
+
16
+ auxiliary_file = Tempfile.new(node.name)
17
+ build_settings = settings_hash_to_string(node.build_settings.filtered)
18
+ save_auxiliary_data(build_settings, dependency_shas, auxiliary_file)
19
+
20
+ input_paths = list_input_paths(node)
21
+ node.sha = calculate_sha(input_paths + [auxiliary_file.path])
22
+ auxiliary_file.close(true)
23
+ end
24
+
25
+ private
26
+
27
+ # @param [XcodeArchiveCache::BuildGraph::Node] node
28
+ #
29
+ # @return [Array<String>]
30
+ # List of input file paths for native target
31
+ #
32
+ def list_input_paths(node)
33
+ inputs = []
34
+
35
+ node.native_target.build_phases.each do |build_phase|
36
+ inputs << list_build_phase_inputs(build_phase)
37
+ end
38
+
39
+ # file path order should not affect evaluation result
40
+ inputs.flatten.compact.sort
41
+ end
42
+
43
+ # @param [Xcodeproj::Project::Object::AbstractBuildPhase] build_phase
44
+ #
45
+ # @return [Array<String>]
46
+ # List of input file paths for build phase
47
+ #
48
+ def list_build_phase_inputs(build_phase)
49
+ build_phase.files_references.map do |file_ref|
50
+ next unless file_ref.is_a?(Xcodeproj::Project::Object::PBXFileReference)
51
+
52
+ begin
53
+ path = file_ref.real_path.to_s
54
+ rescue
55
+ next
56
+ end
57
+
58
+ if File.file?(path)
59
+ next path
60
+ elsif File.directory?(path)
61
+ # NOTE: find doesn't follow symlinks, shouldn't we follow them?
62
+ next Find.find(path).select {|found| File.file?(found)}
63
+ end
64
+
65
+ []
66
+ end
67
+ end
68
+
69
+ # @param [Hash{String => String}] hash
70
+ #
71
+ # @return [String]
72
+ #
73
+ def settings_hash_to_string(hash)
74
+ hash.map {|name, value| "#{name} = #{value}"}.join("\n")
75
+ end
76
+
77
+ # @param [Tempfile] tempfile
78
+ # @param [String] build_settings
79
+ # @param [Array<String>] dependency_shas
80
+ #
81
+ def save_auxiliary_data(build_settings, dependency_shas, tempfile)
82
+ file_contents = build_settings + dependency_shas.join("\n")
83
+ tempfile << file_contents
84
+ tempfile.flush
85
+ end
86
+
87
+ # @param [Array<String>] file_paths
88
+ # File paths to include in resulting sha
89
+ #
90
+ # @return [String] sha256 over specified files
91
+ #
92
+ def calculate_sha(file_paths)
93
+ hash = Digest::SHA256.new
94
+ file_paths.map do |path|
95
+ hash << Digest::SHA256.file(path).hexdigest
96
+ end
97
+
98
+ hash.to_s
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,80 @@
1
+ module XcodeArchiveCache
2
+ module BuildSettings
3
+ class Extractor
4
+
5
+ def initialize
6
+ @parser = Parser.new
7
+ @filter = Filter.new
8
+ end
9
+
10
+ # @param [String] build_settings
11
+ # Raw `xcodebuild -alltargets -showBuildSettings` output
12
+ #
13
+ # @return [Hash{String => Container}]
14
+ # Target build settings keyed by target name
15
+ #
16
+ def extract_per_target(build_settings)
17
+ per_target_settings = build_settings.split("Build settings for action")
18
+ result = Hash.new
19
+
20
+ per_target_settings.each do |target_settings|
21
+ parsed_settings = parse(target_settings)
22
+ target_name = get_target_name(parsed_settings)
23
+ next unless target_name
24
+
25
+ filtered_settings = filter.filter(parsed_settings)
26
+ result[target_name] = Container.new(parsed_settings, filtered_settings)
27
+ end
28
+
29
+ result
30
+ end
31
+
32
+ private
33
+
34
+ # @return [Filter]
35
+ #
36
+ attr_reader :filter
37
+
38
+ # @return [Parser]
39
+ #
40
+ attr_reader :parser
41
+
42
+ # @param [String] settings
43
+ #
44
+ # @return [Hash{String => String}]
45
+ # Setting values keyed by setting names
46
+ #
47
+ def parse(settings)
48
+ # remove leading spaces in each line, then split to lines
49
+ lines = settings.strip.gsub(/\n\s{2,}/, "\n").split("\n")
50
+ # drop "Build settings for action ..."
51
+ lines.drop(1)
52
+
53
+ result = Hash.new
54
+
55
+ lines.each do |line|
56
+ name = parser.parse_name(line)
57
+ next unless name
58
+
59
+ value = parser.parse_value(line)
60
+ next unless value
61
+
62
+ result[name] = value
63
+ end
64
+
65
+ result
66
+ end
67
+
68
+ TARGET_NAME_KEY = "TARGETNAME".freeze
69
+
70
+ # @param [Hash{String => String}] parsed_settings
71
+ #
72
+ # @return [String]
73
+ # Name of target that settings apply to
74
+ #
75
+ def get_target_name(parsed_settings)
76
+ parsed_settings[TARGET_NAME_KEY]
77
+ end
78
+ end
79
+ end
80
+ end