xcode-archive-cache 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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