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,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 49b93ea668f7de9a3723feec1518e934f0b46755afaf941866cf028ed0cc7f0b
4
+ data.tar.gz: 16c0538e9665fe7f7c95c747d34f8ae48a3d8c2c5ed7adb1cc865dbbcbe0c43f
5
+ SHA512:
6
+ metadata.gz: 07424e1f9f50852d8d3a2fb873454d6e441860d33d220a6b37aeb11bb5dd6e90a872bbd37f8c749bb3b0e6a0693fe9aab8fb6c344f7294127861f7ddd9321e8f
7
+ data.tar.gz: 670815a9f5e0bf971f5f0868e42bdeb0b2d0f12b745572688c0022914f57d6d0ba3016cddd54b5aca92203101df21c52b84d6c01de9c080bfa6eb3be1e9b0273
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ if $PROGRAM_NAME == __FILE__
4
+ require 'pathname'
5
+ ROOT = Pathname.new(File.expand_path('../../', __FILE__))
6
+ $LOAD_PATH.unshift((ROOT + 'lib').to_s)
7
+ end
8
+
9
+ require 'xcode-archive-cache'
10
+
11
+ XcodeArchiveCache::Command.run(ARGV)
@@ -0,0 +1,18 @@
1
+ module XcodeArchiveCache
2
+ module ArtifactCache
3
+ class AbstractStorage
4
+
5
+ # @param [XcodeArchiveCache::BuildGraph::Node] node
6
+ #
7
+ # @return [String] cached artifact path, nil if no cached artifact found
8
+ #
9
+ def cached_artifact_path(node)
10
+ nil
11
+ end
12
+
13
+ def store(node, path)
14
+
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,110 @@
1
+ module XcodeArchiveCache
2
+ module ArtifactCache
3
+ class Archiver
4
+
5
+ include XcodeArchiveCache::Logs
6
+
7
+ # @param [String] path
8
+ # @param [String] destination
9
+ #
10
+ def archive(path, destination)
11
+ if File.exists?(destination)
12
+ raise ArgumentError.new, "Artifact cache path #{destination} is already taken"
13
+ end
14
+
15
+ if File.file?(path)
16
+ archive_single_file(path, destination)
17
+ elsif File.directory?(path)
18
+ archive_directory(path, destination)
19
+ else
20
+ raise ArgumentError.new, "No artifact found at path #{path}"
21
+ end
22
+ end
23
+
24
+ # @param [String] path
25
+ # @param [String] destination
26
+ #
27
+ def unarchive(path, destination)
28
+ unless File.file?(path)
29
+ raise ArgumentError.new, "Artifact archive not found: #{path}"
30
+ end
31
+
32
+ unless File.directory?(destination)
33
+ FileUtils.mkdir_p(destination)
34
+ end
35
+
36
+ Zip::File.open(path) do |archive|
37
+ archive.each do |archive_entry|
38
+ destination_file_path = File.join(destination, archive_entry.name)
39
+ destination_dir_path = File.dirname(destination_file_path)
40
+ unless File.exists?(destination_dir_path) && File.directory?(destination_dir_path)
41
+ FileUtils.mkdir_p(destination_dir_path)
42
+ end
43
+
44
+ archive_entry.extract(destination_file_path)
45
+ end
46
+ end
47
+ end
48
+
49
+ private
50
+
51
+ # @param [String] path
52
+ # @param [String] destination
53
+ #
54
+ def archive_single_file(path, destination)
55
+ info("archiving #{path}")
56
+
57
+ Zip::File.open(destination, Zip::File::CREATE) do |archive|
58
+ archive.add(File.basename(path), path)
59
+ end
60
+ end
61
+
62
+ # @param [String] path
63
+ # @param [String] destination
64
+ #
65
+ def archive_directory(path, destination)
66
+ info("archiving #{path}")
67
+
68
+ Zip::File.open(destination, Zip::File::CREATE) do |archive|
69
+ add_entries(list_entries_in_directory(path), path, archive)
70
+ end
71
+ end
72
+
73
+ # @param [Array<String>] entries
74
+ # @param [String] root_dir
75
+ # @param [Zip::File] archive
76
+ #
77
+ def add_entries(entries, root_dir, archive)
78
+ entries.each do |entry|
79
+ if File.directory?(entry)
80
+ add_entries(list_entries_in_directory(entry), root_dir, archive)
81
+ elsif File.file?(entry)
82
+ add_single_file_entry(entry, root_dir, archive)
83
+ else
84
+ raise ArgumentError.new, "No file found at path #{entry}"
85
+ end
86
+ end
87
+ end
88
+
89
+ # @param [String] path
90
+ #
91
+ def list_entries_in_directory(path)
92
+ (Dir.entries(path) - %w(. ..)).map {|entry| File.join(path, entry)}
93
+ end
94
+
95
+ # @param [String] path_on_disk
96
+ # @param [String] root_dir
97
+ # @param [Zip::File] archive
98
+ #
99
+ def add_single_file_entry(path_on_disk, root_dir, archive)
100
+ file_path = Pathname.new(path_on_disk)
101
+ root_dir_path = Pathname.new(root_dir)
102
+ path_in_archive = file_path.relative_path_from(root_dir_path).to_s
103
+
104
+ archive.get_output_stream(path_in_archive) do |stream|
105
+ stream.write(File.open(path_on_disk, 'rb').read)
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,31 @@
1
+ module XcodeArchiveCache
2
+ module ArtifactCache
3
+ class ArtifactExtractor
4
+
5
+ # @param [XcodeArchiveCache::ArtifactCache::AbstractStorage] storage
6
+ #
7
+ def initialize(storage)
8
+ @storage = storage
9
+ @archiver = Archiver.new
10
+ end
11
+
12
+ # @param [XcodeArchiveCache::BuildGraph::Node] node
13
+ # @param [String] destination
14
+ #
15
+ def unpack(node, destination)
16
+ cached_artifact_path = storage.cached_artifact_path(node)
17
+ archiver.unarchive(cached_artifact_path, destination)
18
+ end
19
+
20
+ private
21
+
22
+ # @return [XcodeArchiveCache::ArtifactCache::AbstractStorage]
23
+ #
24
+ attr_reader :storage
25
+
26
+ # @return [XcodeArchiveCache::ArtifactCache::Archiver]
27
+ #
28
+ attr_reader :archiver
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,46 @@
1
+ module XcodeArchiveCache
2
+ module ArtifactCache
3
+ class LocalStorage < AbstractStorage
4
+
5
+ # @param [String] cache_dir_path
6
+ #
7
+ def initialize(cache_dir_path)
8
+ @cache_dir_path = cache_dir_path
9
+ @archiver = Archiver.new
10
+ end
11
+
12
+ # @param [XcodeArchiveCache::BuildGraph::Node] node
13
+ #
14
+ # @return [String] cached artifact path, nil if no artifact found in cache dir
15
+ #
16
+ def cached_artifact_path(node)
17
+ path = path_inside_cache_dir(node)
18
+ File.exist?(path) ? path : nil
19
+ end
20
+
21
+ def store(node, path)
22
+ archive_path = path_inside_cache_dir(node)
23
+ archive_directory = File.expand_path("..", archive_path)
24
+ unless File.exist?(archive_directory)
25
+ FileUtils.mkdir_p(archive_directory)
26
+ end
27
+
28
+ archiver.archive(path, archive_path)
29
+ end
30
+
31
+ private
32
+
33
+ # @return [String]
34
+ #
35
+ attr_reader :cache_dir_path
36
+
37
+ # @return [XcodeArchiveCache::ArtifactCache::Archiver]
38
+ #
39
+ attr_reader :archiver
40
+
41
+ def path_inside_cache_dir(node)
42
+ File.join(cache_dir_path, node.name, node.sha)
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,48 @@
1
+ module XcodeArchiveCache
2
+ module Build
3
+ class Performer
4
+
5
+ include XcodeArchiveCache::Logs
6
+
7
+ # @param [String] derived_data_path
8
+ #
9
+ def initialize(xcodebuild_executor, derived_data_path)
10
+ @xcodebuild_executor = xcodebuild_executor
11
+ @derived_data_path = derived_data_path
12
+ end
13
+
14
+ # @param [String] configuration
15
+ # @param [Xcodeproj::Project::Object::PBXNativeTarget] target
16
+ # @param [XcodeArchiveCache::BuildGraph::Graph] graph
17
+ #
18
+ def rebuild_missing(target, graph)
19
+ should_rebuild_anything = should_rebuild?(graph)
20
+ if should_rebuild_anything
21
+ rebuild_list = graph.nodes.select(&:rebuild).map(&:name).join(", ")
22
+ info("going to rebuild:\n#{rebuild_list}")
23
+
24
+ build_result = xcodebuild_executor.build(target.project.path, target.name, derived_data_path)
25
+ unless build_result
26
+ raise StandardError.new, "Failed to perform rebuild"
27
+ end
28
+ else
29
+ info("no need to rebuild anything")
30
+ end
31
+ end
32
+
33
+ # @param [XcodeArchiveCache::BuildGraph::Graph] graph
34
+ #
35
+ def should_rebuild?(graph)
36
+ graph.nodes.reduce(false) {|rebuild, node| rebuild || node.rebuild}
37
+ end
38
+
39
+ private
40
+
41
+ # @return [String]
42
+ #
43
+ attr_reader :derived_data_path
44
+
45
+ attr_reader :xcodebuild_executor
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,129 @@
1
+ module XcodeArchiveCache
2
+ module Build
3
+ class ProductExtractor
4
+
5
+ # @param [String] configuration
6
+ # @param [String] derived_data_path
7
+ #
8
+ def initialize(configuration, derived_data_path)
9
+ @configuration = configuration
10
+ @derived_data_path = derived_data_path
11
+ @shell_executor = XcodeArchiveCache::Shell::Executor.new
12
+ end
13
+
14
+ def list_product_contents(root_target_name, built_node)
15
+ file_paths = list_products(root_target_name, built_node)
16
+ file_paths.select {|path| File.exist?(path)}.map {|path| File.realpath(path) }
17
+ end
18
+
19
+ private
20
+
21
+ # @return [String]
22
+ #
23
+ attr_reader :configuration
24
+
25
+ # @return [String]
26
+ #
27
+ attr_reader :derived_data_path
28
+
29
+ # @return [XcodeArchiveCache::Shell::Executor]
30
+ #
31
+ attr_reader :shell_executor
32
+
33
+ # @param [XcodeArchiveCache::BuildGraph::Node] built_node
34
+ #
35
+ def list_products(root_target_name, built_node)
36
+ if built_node.has_framework_product?
37
+ list_framework_products(root_target_name, built_node)
38
+ elsif built_node.has_static_library_product?
39
+ list_static_lib_products(root_target_name, built_node)
40
+ end
41
+ end
42
+
43
+ # @param [XcodeArchiveCache::BuildGraph::Node] built_node
44
+ #
45
+ # @return [Array<String>]
46
+ #
47
+ def list_framework_products(root_target_name, built_node)
48
+ framework_glob = get_main_product_glob(root_target_name, built_node)
49
+ framework_path = Dir.glob(framework_glob).first
50
+ unless framework_path
51
+ raise Informative, "Framework product not found for #{built_node.name}"
52
+ end
53
+
54
+ framework_dsym_glob = File.join(File.dirname(framework_glob), built_node.dsym_file_name)
55
+ framework_dsym_path = Dir.glob(framework_dsym_glob).first
56
+
57
+ bc_symbolmap_paths = list_framework_bc_symbolmaps(framework_path)
58
+
59
+ ([framework_path, framework_dsym_path] + bc_symbolmap_paths).flatten.compact
60
+ end
61
+
62
+ # @param [XcodeArchiveCache::BuildGraph::Node] built_node
63
+ #
64
+ # @return [Array<String>]
65
+ #
66
+ def list_static_lib_products(root_target_name, built_node)
67
+ static_lib_glob = get_main_product_glob(root_target_name, built_node)
68
+ static_lib_path = Dir.glob(static_lib_glob).first
69
+ unless static_lib_path
70
+ raise Informative, "Static library product not found for #{built_node.name}"
71
+ end
72
+
73
+ [static_lib_path]
74
+ end
75
+
76
+ # @param [XcodeArchiveCache::BuildGraph::Node] built_node
77
+ #
78
+ # @return [String]
79
+ #
80
+ def get_main_product_glob(root_target_name, built_node)
81
+ product_name = built_node.native_target.product_reference.name ?
82
+ built_node.native_target.product_reference.name :
83
+ built_node.native_target.product_reference.path
84
+ File.join(derived_data_path,
85
+ "**",
86
+ File.basename(product_name))
87
+ end
88
+
89
+ # @param [XcodeArchiveCache::BuildGraph::Node] built_node
90
+ #
91
+ # @return [String]
92
+ #
93
+ def configuration_dir(built_node)
94
+ "#{configuration}-#{built_node.native_target.sdk}"
95
+ end
96
+
97
+ # @param [String] framework_path
98
+ #
99
+ # @return [Array<String>]
100
+ #
101
+ def list_framework_bc_symbolmaps(framework_path)
102
+ executable_name = File.basename(framework_path, File.extname(framework_path))
103
+ executable_path = File.join(framework_path, executable_name)
104
+ unless File.exist?(executable_path)
105
+ raise Informative, "Failed to find executable inside framework: #{framework_path}"
106
+ end
107
+
108
+ uuids = list_bc_symbolmap_uuids(executable_path)
109
+ uuids.map {|uuid| find_bc_symbolmap(uuid)}.flatten
110
+ end
111
+
112
+ # @param [String] executable_path
113
+ #
114
+ # @return [Array<String>]
115
+ #
116
+ def list_bc_symbolmap_uuids(executable_path)
117
+ shell_executor.execute_for_output("otool -l #{executable_path} | grep uuid | awk {'print $2'}").split("\n")
118
+ end
119
+
120
+ # @param [String] uuid
121
+ #
122
+ # @return [String]
123
+ #
124
+ def find_bc_symbolmap(uuid)
125
+ Dir.glob(File.join(derived_data_path, "**", "#{uuid}.bcsymbolmap")).first
126
+ end
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,154 @@
1
+ module XcodeArchiveCache
2
+ module BuildGraph
3
+ class Builder
4
+
5
+ include XcodeArchiveCache::Logs
6
+
7
+ # @param [XcodeArchiveCache::Xcodebuild::Executor] xcodebuild_executor
8
+ #
9
+ def initialize(native_target_finder, xcodebuild_executor)
10
+ @build_settings_loader = XcodeArchiveCache::BuildSettings::Loader.new(xcodebuild_executor)
11
+ @native_target_finder = native_target_finder
12
+ @sha_calculator = NodeShaCalculator.new
13
+ end
14
+
15
+ # @param [Xcodeproj::Project::Object::PBXNativeTarget] dependency_target
16
+ #
17
+ # @return [Graph]
18
+ #
19
+ def build_graph(dependent_target, dependency_target)
20
+ native_target_finder.set_platform_name_filter(dependency_target.platform_name)
21
+
22
+ build_settings = load_setting_for_target(dependent_target)
23
+ graph = Graph.new(dependency_target.project, build_settings)
24
+
25
+ add_to_graph(dependency_target, graph, true)
26
+ load_settings(graph)
27
+ calculate_shas(graph)
28
+
29
+ graph
30
+ end
31
+
32
+ private
33
+
34
+ # @return [XcodeArchiveCache::BuildSettings::Loader]
35
+ #
36
+ attr_accessor :build_settings_loader
37
+
38
+ # @return [XcodeArchive::BuildGraph::NativeTargetFinder]
39
+ #
40
+ attr_accessor :native_target_finder
41
+
42
+ # @return [XcodeArchiveCache::BuildGraph::NodeShaCalculator]
43
+ #
44
+ attr_accessor :sha_calculator
45
+
46
+ # @param [Xcodeproj::Project::Object::PBXNativeTarget] target
47
+ # @param [Graph] graph
48
+ # @param [Boolean] is_root
49
+ # @param [Array<String>] target_stack
50
+ # Stack of native target display names at this level of traverse
51
+ #
52
+ # @return [Node] new or existing node
53
+ #
54
+ def add_to_graph(target, graph, is_root, target_stack = [])
55
+ debug("traversing #{target.display_name}")
56
+
57
+ unless target
58
+ raise ArgumentError.new, "Target is required"
59
+ end
60
+
61
+ display_name = target.display_name
62
+ existing_node = graph.node_by_name(display_name)
63
+ if existing_node
64
+ debug("already added this one")
65
+ return existing_node
66
+ end
67
+
68
+ if target_stack.include?(display_name)
69
+ target_stack.push(display_name)
70
+ raise Informative, "Circular dependency detected: #{target_stack.join(" -> ")}"
71
+ end
72
+
73
+ node = graph.node_by_name(display_name)
74
+ if node
75
+ debug("already traversed this one")
76
+ return node
77
+ else
78
+ debug("adding new node")
79
+ node = Node.new(display_name, target, is_root)
80
+ graph.nodes.push(node)
81
+ end
82
+
83
+ dependencies = []
84
+ target_stack.push(display_name)
85
+
86
+ dependency_targets = target.dependencies.map {|dependency| native_target_finder.find_for_dependency(dependency)} +
87
+ target.frameworks_build_phase.files.map {|file| native_target_finder.find_for_file(file)}
88
+
89
+ # PBXNativeTarget has no custom equality check
90
+ deduplicated_targets = dependency_targets.compact.uniq {|dependency_target| dependency_target.uuid + dependency_target.display_name}
91
+ debug("dependency targets: #{deduplicated_targets.map(&:display_name)}")
92
+
93
+ deduplicated_targets.each do |dependency_target|
94
+ dependency_node = add_to_graph(dependency_target, graph, false, target_stack)
95
+
96
+ unless dependency_node.dependent.include?(node)
97
+ debug("adding #{node.name} as dependent to #{dependency_node.name}")
98
+ dependency_node.dependent.push(node)
99
+ end
100
+
101
+ unless dependencies.include?(dependency_node)
102
+ debug("adding #{dependency_node.name} as dependency to #{node.name}")
103
+ dependencies.push(dependency_node)
104
+ end
105
+ end
106
+
107
+ target_stack.pop
108
+ node.dependencies.push(*dependencies)
109
+
110
+ debug("done with #{target.display_name}")
111
+ node
112
+ end
113
+
114
+ # @param [XcodeArchiveCache::BuildGraph::Graph] graph
115
+ #
116
+ def calculate_shas(graph)
117
+ graph.nodes.each do |node|
118
+ debug("calculating sha for #{node.name}")
119
+ sha_calculator.calculate(node)
120
+ debug("sha calculated for #{node.name}: #{node.sha}")
121
+ end
122
+ end
123
+
124
+ # @param [XcodeArchiveCache::BuildGraph::Graph] graph
125
+ #
126
+ def load_settings(graph)
127
+ counter = 1
128
+
129
+ graph.nodes.each do |node|
130
+ node_settings = load_setting_for_target(node.native_target)
131
+ unless node_settings
132
+ raise Informative, "No build settings loaded for #{node.name}"
133
+ end
134
+
135
+ node.build_settings = node_settings
136
+
137
+ debug("settings loaded for #{node.name} (#{counter} / #{graph.nodes.length})")
138
+ counter += 1
139
+ end
140
+ end
141
+
142
+
143
+ # @param [Xcodeproj::Project::Object::PBXNativeTarget] target
144
+ #
145
+ def load_setting_for_target(target)
146
+ info("loading settings for #{target.display_name}")
147
+
148
+ project_path = target.project.path
149
+ build_settings_loader.load_settings(project_path)
150
+ build_settings_loader.get_settings(project_path, target.display_name)
151
+ end
152
+ end
153
+ end
154
+ end