carthage_remote_cache 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,65 @@
1
+ class CarthageDependency
2
+
3
+ class << self
4
+ # Parses Cartfile.resolved dependency entry, e.g.
5
+ # github "CocoaLumberjack/CocoaLumberjack" "3.2.1"
6
+ def parse_cartfile_resolved_line(line)
7
+ line.strip!
8
+ matches = line.match(/^(\w+)\s+\"([^\"]+)\"(\s+\"([^\"]+)\")$/)
9
+ return nil if matches.nil?
10
+ if matches.length == 5
11
+ CarthageDependency.new(type: matches[1], repository: matches[2], version: matches[4])
12
+ else
13
+ nil
14
+ end
15
+ end
16
+ end
17
+
18
+ attr_reader :type, :repository, :version
19
+
20
+ def initialize(args)
21
+ @type = args[:type]
22
+ @repository = args[:repository]
23
+ @version = args[:version]
24
+ end
25
+
26
+ # Since one Cartfile.resolved entry may produce multiple differently named frameworks,
27
+ # this is an entry point to identifying a framework name.
28
+ def guessed_framework_basename
29
+ case @type
30
+ when "github"
31
+ repository.split("/").last
32
+ else
33
+ raise "Determining version_filename from #{@type} dependency is not yet supported"
34
+ end
35
+ end
36
+
37
+ def version_filename
38
+ ".#{guessed_framework_basename}.version"
39
+ end
40
+
41
+ def version_filepath
42
+ File.join(CARTHAGE_BUILD_DIR, version_filename)
43
+ end
44
+
45
+ def validate_version_file(version_file)
46
+ raise OutdatedFrameworkBuildError.new, version_validation_message(version_file) if @version != version_file.version
47
+ end
48
+
49
+ def to_s
50
+ "#{@type} \"#{@repository}\" \"#{@version}\""
51
+ end
52
+
53
+ private
54
+
55
+ def version_validation_message(version_file)
56
+ <<~EOS
57
+ Outdated version of '#{guessed_framework_basename}' framework detected:
58
+ Expected version '#{@version}'
59
+ Found version '#{version_file.version}'
60
+
61
+ Please run `carthage bootstrap` to build frameworks.
62
+ EOS
63
+ end
64
+
65
+ end
@@ -0,0 +1,13 @@
1
+ lib = File.expand_path("..", __FILE__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+
4
+ require 'api'
5
+ require 'carthage_archive'
6
+ require 'carthage_dependency'
7
+ require 'constants'
8
+ require 'configuration'
9
+ require 'errors'
10
+ require 'log'
11
+ require 'networking'
12
+ require 'utils'
13
+ require 'version_file'
@@ -0,0 +1,67 @@
1
+ require 'concurrent'
2
+
3
+ class DownloadCommand
4
+
5
+ def initialize(options)
6
+ @options = options
7
+ @config = Configuration.new
8
+ @networking = Networking.new(@config)
9
+ @api = API.new(@networking, options)
10
+ end
11
+
12
+ def run
13
+ pool = Concurrent::FixedThreadPool.new(THREAD_POOL_SIZE)
14
+
15
+ @number_of_downloaded_archives = 0
16
+ @number_of_skipped_archives = 0
17
+ errors = Concurrent::Array.new
18
+
19
+ for carthage_dependency in @config.carthage_dependencies
20
+ pool.post(carthage_dependency) do |carthage_dependency|
21
+ begin
22
+ download(carthage_dependency)
23
+ rescue => e
24
+ errors << e
25
+ end
26
+ end
27
+ end
28
+
29
+ pool.shutdown
30
+ pool.wait_for_termination
31
+
32
+ if errors.count > 0
33
+ raise MultipleErrorsError.new(errors)
34
+ else
35
+ puts "Downloaded and extracted #{@number_of_downloaded_archives} archives, skipped #{@number_of_skipped_archives} archives."
36
+ end
37
+ end
38
+
39
+ private
40
+
41
+ def download(carthage_dependency)
42
+ local_version_file = if File.exist?(carthage_dependency.version_filepath)
43
+ VersionFile.new(carthage_dependency.version_filepath)
44
+ else
45
+ nil
46
+ end
47
+
48
+ if !local_version_file.nil? && @api.version_file_matches_server?(carthage_dependency, local_version_file)
49
+ $LOG.debug("Version file #{local_version_file.path} matches server version, skipping download")
50
+ @number_of_skipped_archives += local_version_file.number_of_frameworks
51
+ return
52
+ end
53
+
54
+ version_file = @networking.download_version_file(carthage_dependency)
55
+ raise "Version file #{carthage_dependency.version_filename} is not present on the server, please run `carthagerc upload` first" if version_file.nil?
56
+
57
+ version_file.frameworks_by_platform.each do |platform, framework_names|
58
+ for framework_name in framework_names do
59
+ archive = @api.download_and_unpack_archive(carthage_dependency, framework_name, platform)
60
+ raise "Failed to download framework #{carthage_dependency} – #{framework_name} (#{platform}). Please `upload` the framework first." if archive.nil?
61
+ @number_of_downloaded_archives += 1
62
+ end
63
+ end
64
+ version_file.move_to_build_dir
65
+ end
66
+
67
+ end
@@ -0,0 +1,26 @@
1
+ class InitCommand
2
+
3
+ def initialize(options)
4
+ @options = options
5
+ end
6
+
7
+ def run
8
+ path = File.join(Dir.pwd, CARTRCFILE)
9
+ if File.exist?(path)
10
+ raise "File #{path} already exists"
11
+ else
12
+ File.write(path, file_contents)
13
+ end
14
+ end
15
+
16
+ private
17
+
18
+ def file_contents
19
+ <<~EOS
20
+ Configuration.setup do |c|
21
+ c.server = "http://localhost:#{SERVER_DEFAULT_PORT}/"
22
+ end
23
+ EOS
24
+ end
25
+
26
+ end
@@ -0,0 +1,16 @@
1
+ class ServerCommand
2
+
3
+ def initialize(options)
4
+ @options = options
5
+ end
6
+
7
+ def run
8
+ ENV['RACK_ENV'] = 'production'
9
+ require 'server/server_app'
10
+ Rack::Handler::WEBrick.run(
11
+ Sinatra::Application,
12
+ :Port => @options[:server_port]
13
+ )
14
+ end
15
+
16
+ end
@@ -0,0 +1,63 @@
1
+ require 'concurrent'
2
+
3
+ class UploadCommand
4
+
5
+ def initialize(options)
6
+ @config = Configuration.new
7
+ @networking = Networking.new(@config)
8
+ @api = API.new(@networking, options)
9
+ end
10
+
11
+ def run
12
+ pool = Concurrent::FixedThreadPool.new(THREAD_POOL_SIZE)
13
+
14
+ $LOG.debug("Will upload frameworks: #{@config.all_framework_names}")
15
+
16
+ @number_of_uploaded_archives = 0
17
+ @number_of_skipped_archives = 0
18
+ errors = Concurrent::Array.new
19
+
20
+ for carthage_dependency in @config.carthage_dependencies
21
+ pool.post(carthage_dependency) do |carthage_dependency|
22
+ begin
23
+ upload(carthage_dependency)
24
+ rescue => e
25
+ errors << e
26
+ end
27
+ end
28
+ end
29
+
30
+ pool.shutdown
31
+ pool.wait_for_termination
32
+
33
+ if errors.count > 0
34
+ raise MultipleErrorsError.new(errors)
35
+ else
36
+ puts "Uploaded #{@number_of_uploaded_archives} archives, skipped #{@number_of_skipped_archives}."
37
+ end
38
+ end
39
+
40
+ private
41
+
42
+ def upload(carthage_dependency)
43
+ version_file = VersionFile.new(carthage_dependency.version_filepath)
44
+
45
+ carthage_dependency.validate_version_file(version_file)
46
+
47
+ if @api.version_file_matches_server?(carthage_dependency, version_file)
48
+ $LOG.debug("Version file #{version_file.path} matches server version, skipping upload")
49
+ @number_of_skipped_archives += version_file.number_of_frameworks
50
+ return
51
+ end
52
+
53
+ @networking.upload_version_file(carthage_dependency)
54
+
55
+ version_file.frameworks_by_platform.each do |platform, framework_names|
56
+ for framework_name in framework_names
57
+ @api.create_and_upload_archive(carthage_dependency, framework_name, platform)
58
+ @number_of_uploaded_archives += 1
59
+ end
60
+ end
61
+ end
62
+
63
+ end
data/lib/commands.rb ADDED
@@ -0,0 +1,7 @@
1
+ lib = File.expand_path("..", __FILE__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+
4
+ require 'commands/download_command'
5
+ require 'commands/init_command'
6
+ require 'commands/server_command'
7
+ require 'commands/upload_command'
@@ -0,0 +1,81 @@
1
+ require 'uri'
2
+
3
+ class Configuration
4
+
5
+ class UserConfig
6
+ attr_accessor :server
7
+ end
8
+
9
+ @@user_config = UserConfig.new
10
+
11
+ def self.setup
12
+ yield(@@user_config)
13
+ end
14
+
15
+ attr_reader :xcodebuild_version, :swift_version, :carthage_dependencies, :server_uri
16
+
17
+ def initialize
18
+ initialize_env
19
+ initialize_cartrcfile
20
+ end
21
+
22
+ def all_framework_names
23
+ version_files.flat_map { |vf| vf.framework_names }.uniq.sort
24
+ end
25
+
26
+ def to_s
27
+ <<~EOS
28
+ Xcodebuild: #{@xcodebuild_version}
29
+ ---
30
+ Swift: #{@swift_version}
31
+ ---
32
+ Server: #{@server_uri.to_s}
33
+ ---
34
+ Cartfile.resolved:
35
+ #{@carthage_dependencies.join("\n")}
36
+ ---
37
+ Local Build Frameworks:
38
+ #{framework_names_with_platforms.join("\n")}
39
+ EOS
40
+ end
41
+
42
+ private
43
+
44
+ def initialize_env
45
+ xcodebuild_raw_version = sh("xcodebuild -version")
46
+ @xcodebuild_version = xcodebuild_raw_version[/Build version (.*)$/, 1]
47
+ raise "Could not parse build version from '#{xcodebuild_raw_version}'" if @xcodebuild_version.nil?
48
+
49
+ swift_raw_version = sh("swift -version")
50
+ @swift_version = swift_raw_version[/Apple Swift version (.*) \(/, 1]
51
+ raise "Could not parse swift version from '#{raw_swift_version}'" if @swift_version.nil?
52
+
53
+ raise "Misssing #{CARTFILE_RESOLVED}" unless File.exist?(CARTFILE_RESOLVED)
54
+ @carthage_dependencies = File.readlines(CARTFILE_RESOLVED)
55
+ .map { |line| CarthageDependency.parse_cartfile_resolved_line(line) }
56
+ .compact
57
+ end
58
+
59
+ def initialize_cartrcfile
60
+ raise "Configuration file #{CARTRCFILE} was not found, consider creating one by running `carthagerc init`" unless File.exist?(CARTRCFILE)
61
+
62
+ # Populate class variable @@user_config.
63
+ load File.join(Dir.pwd, CARTRCFILE)
64
+
65
+ raise "Missing 'server' configuration in #{CARTRCFILE}" if @@user_config.server.nil? || @@user_config.server.empty?
66
+ @server_uri = URI.parse(@@user_config.server)
67
+ end
68
+
69
+ def framework_names_with_platforms
70
+ version_files.flat_map do |vf|
71
+ vf.platforms_by_framework.flat_map do |framework_name, platforms|
72
+ "#{framework_name} #{vf.version} #{platforms}"
73
+ end
74
+ end
75
+ end
76
+
77
+ def version_files
78
+ @carthage_dependencies.map { |d| VersionFile.new(d.version_filepath) }
79
+ end
80
+
81
+ end
data/lib/constants.rb ADDED
@@ -0,0 +1,8 @@
1
+ CARTHAGE_DIR = 'Carthage'
2
+ CARTHAGE_BUILD_DIR = File.join(CARTHAGE_DIR, 'Build')
3
+ CARTFILE_RESOLVED = 'Cartfile.resolved'
4
+ CARTRCFILE = 'Cartrcfile'
5
+ THREAD_POOL_SIZE = 8
6
+
7
+ SERVER_DEFAULT_PORT = 9292
8
+ SERVER_CACHE_DIR = File.join(Dir.home, '.carthage-remote-cache')
data/lib/errors.rb ADDED
@@ -0,0 +1,13 @@
1
+ class MultipleErrorsError < StandardError
2
+
3
+ def initialize(errors)
4
+ @errors = errors
5
+ end
6
+
7
+ def message
8
+ @errors.map { |e| e.message }.join("\n")
9
+ end
10
+
11
+ end
12
+
13
+ class OutdatedFrameworkBuildError < StandardError; end
data/lib/log.rb ADDED
@@ -0,0 +1,4 @@
1
+ require 'logger'
2
+
3
+ $LOG = Logger.new(STDOUT)
4
+ $LOG.level = Logger::INFO
data/lib/networking.rb ADDED
@@ -0,0 +1,96 @@
1
+ require 'rest-client'
2
+ require 'uri'
3
+
4
+ class Networking
5
+
6
+ def initialize(config)
7
+ @config = config
8
+ end
9
+
10
+ # Version Files
11
+
12
+ def download_version_file(carthage_dependency)
13
+ url = new_version_file_url(carthage_dependency)
14
+ $LOG.debug("Downloading version file from #{url}")
15
+ version_file = RestClient.get(url) do |response, request, result|
16
+ if response.code == 200
17
+ File.write(carthage_dependency.version_filename, response.to_s)
18
+ VersionFile.new(carthage_dependency.version_filename)
19
+ else
20
+ nil
21
+ end
22
+ end
23
+ version_file
24
+ end
25
+
26
+ def upload_version_file(carthage_dependency)
27
+ url = new_version_file_url(carthage_dependency)
28
+ $LOG.debug("Uploading #{carthage_dependency.version_filename}")
29
+ RestClient.post(url, :version_file => File.new(carthage_dependency.version_filepath))
30
+ end
31
+
32
+ # Archives
33
+
34
+ def download_framework_archive(carthage_dependency, framework_name, platform)
35
+ url = new_framework_url(carthage_dependency, framework_name, platform)
36
+ $LOG.debug("Downloading framework from #{url}")
37
+ archive = RestClient.get(url) do |response, request, result|
38
+ if response.code == 200
39
+ archive = CarthageArchive.new(framework_name, platform)
40
+ File.write(archive.archive_path, response.to_s)
41
+ archive
42
+ else
43
+ nil
44
+ end
45
+ end
46
+ archive
47
+ end
48
+
49
+ def upload_framework_archive(zipfile_name, carthage_dependency, framework_name, platform)
50
+ url = new_framework_url(carthage_dependency, framework_name, platform)
51
+ $LOG.debug("Uploading framework to #{url}")
52
+ RestClient.post(url, :framework_file => File.new(zipfile_name))
53
+ end
54
+
55
+ private
56
+
57
+ def new_version_file_url(carthage_dependency)
58
+ new_server_url([
59
+ 'versions',
60
+ @config.xcodebuild_version,
61
+ @config.swift_version,
62
+ carthage_dependency.guessed_framework_basename,
63
+ carthage_dependency.version,
64
+ carthage_dependency.version_filename,
65
+ ])
66
+ end
67
+
68
+ def new_framework_url(carthage_dependency, framework_name, platform)
69
+ new_server_url([
70
+ 'frameworks',
71
+ @config.xcodebuild_version,
72
+ @config.swift_version,
73
+ carthage_dependency.guessed_framework_basename,
74
+ carthage_dependency.version,
75
+ framework_name,
76
+ platform_to_api_string(platform),
77
+ ])
78
+ end
79
+
80
+ def new_server_url(path_slices)
81
+ sanitized_path_slices = path_slices.map { |p| sanitized(p) }
82
+ uri = URI::HTTP.build(
83
+ :scheme => @config.server_uri.scheme,
84
+ :host => @config.server_uri.host,
85
+ :port => @config.server_uri.port,
86
+ :path => '/' + sanitized_path_slices.join('/')
87
+ )
88
+ uri.to_s
89
+ end
90
+
91
+ # Mangle identifiers for URL paths.
92
+ def sanitized(input)
93
+ input.gsub(/\//, '_')
94
+ end
95
+
96
+ end
@@ -0,0 +1,6 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+ Bundler.require
4
+
5
+ require './server_app.rb'
6
+ run Sinatra::Application
@@ -0,0 +1,93 @@
1
+ require 'sinatra'
2
+ require 'fileutils'
3
+ require 'carthage_remote_cache'
4
+
5
+ get '/' do
6
+ "Welcome to carthage_remote_cache"
7
+ end
8
+
9
+ versions_path = '/versions/:xcodebuild_version/:swift_version/:dependency_name/:version/:version_filename'
10
+ frameworks_path = '/frameworks/:xcodebuild_version/:swift_version/:dependency_name/:version/:framework_name/:platform'
11
+
12
+ get versions_path do
13
+ dirname = params_to_framework_dir(params)
14
+ filename = params[:version_filename]
15
+ filepath = File.join(dirname, filename)
16
+
17
+ if File.exist?(filepath)
18
+ status(200)
19
+ send_file(filepath)
20
+ else
21
+ status(404)
22
+ end
23
+ end
24
+
25
+ post versions_path do
26
+ dirname = params_to_framework_dir(params)
27
+ filename = params[:version_filename]
28
+
29
+ source_file = params[:version_file][:tempfile]
30
+ target_filename = File.join(dirname, filename)
31
+
32
+ FileUtils.mkdir_p(dirname) unless File.directory?(dirname)
33
+ File.delete(target_filename) if File.exist?(target_filename)
34
+
35
+ $LOG.info("Writing #{target_filename}")
36
+ File.open(target_filename, 'wb') do |target_file|
37
+ target_file.write(source_file.read)
38
+ end
39
+
40
+ status(200)
41
+ end
42
+
43
+ # Check whether framework archive is already cached.
44
+ head frameworks_path do
45
+ dirname = params_to_framework_dir(params)
46
+ archive = CarthageArchive.new(params[:framework_name], params[:platform].to_sym)
47
+ filename = File.join(dirname, archive.archive_filename)
48
+
49
+ if File.exist?(filename)
50
+ status(200)
51
+ else
52
+ status(404)
53
+ end
54
+ end
55
+
56
+ # Retrieve .zip framework archive.
57
+ get frameworks_path do
58
+ dirname = params_to_framework_dir(params)
59
+ archive = CarthageArchive.new(params[:framework_name], params[:platform].to_sym)
60
+ filename = File.join(dirname, archive.archive_filename)
61
+
62
+ if File.exist?(filename)
63
+ status(200)
64
+ send_file(filename)
65
+ else
66
+ status(404)
67
+ end
68
+ end
69
+
70
+ # Upload framework archive. Overwrites already cached archive if exists.
71
+ post frameworks_path do
72
+ filename = params[:framework_file][:filename]
73
+ source_file = params[:framework_file][:tempfile]
74
+
75
+ dirname = params_to_framework_dir(params)
76
+ target_filename = File.join(dirname, filename)
77
+
78
+ FileUtils.mkdir_p(dirname) unless File.directory?(dirname)
79
+ File.delete(target_filename) if File.exist?(target_filename)
80
+
81
+ $LOG.info("Writing: #{target_filename}")
82
+ File.open(target_filename, 'wb') do |target_file|
83
+ target_file.write(source_file.read)
84
+ end
85
+
86
+ status(200)
87
+ end
88
+
89
+ private
90
+
91
+ def params_to_framework_dir(params)
92
+ File.join(SERVER_CACHE_DIR, params[:xcodebuild_version], params[:swift_version], params[:dependency_name], params[:version])
93
+ end
data/lib/utils.rb ADDED
@@ -0,0 +1,62 @@
1
+ def sh(cmd)
2
+ output = `#{cmd}`
3
+ bail("Command `#{cmd}` failed!") unless $?.success?
4
+ output.strip
5
+ end
6
+
7
+ # Exits Ruby process, only to be called:
8
+ # 1. If sh / system calls fail
9
+ # 2. From top level `carthagerc` script
10
+ def bail(message, code = 1)
11
+ $stderr.puts(message.strip + "\n")
12
+ Process.exit(code)
13
+ end
14
+
15
+ # Quote command line arguments with double quotes.
16
+ # Useful for file paths with spaces.
17
+ def quote(input)
18
+ if input.is_a? String
19
+ if input.empty?
20
+ ''
21
+ else
22
+ '"' + input + '"'
23
+ end
24
+ elsif input.is_a? Array
25
+ input
26
+ .map { |e| quote(e) }
27
+ .select { |e| !e.empty? }
28
+ .join(' ')
29
+ else
30
+ raise "Unsupported type #{input}"
31
+ end
32
+ end
33
+
34
+ def platform_to_api_string(platform)
35
+ case platform
36
+ when :iOS
37
+ 'iOS'
38
+ when :macOS
39
+ 'macOS'
40
+ when :tvOS
41
+ 'tvOS'
42
+ when :watchOS
43
+ 'watchOS'
44
+ else
45
+ raise "Unrecognized platform #{platform.inspect}"
46
+ end
47
+ end
48
+
49
+ def platform_to_carthage_dir_string(platform)
50
+ case platform
51
+ when :iOS
52
+ 'iOS'
53
+ when :macOS
54
+ 'Mac'
55
+ when :tvOS
56
+ 'tvOS'
57
+ when :watchOS
58
+ 'watchOS'
59
+ else
60
+ raise "Unrecognized platform #{platform.inspect}"
61
+ end
62
+ end