mirror of
https://github.com/zhigang1992/fir-cli.git
synced 2026-04-28 17:44:55 +08:00
Merge branch 'develop'
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -8,6 +8,7 @@
|
||||
/spec/reports/
|
||||
/tmp/
|
||||
/fir-cli_tmp/
|
||||
/test/build_project/
|
||||
*.bundle
|
||||
*.so
|
||||
*.o
|
||||
|
||||
101
lib/fir.rb
101
lib/fir.rb
@@ -23,106 +23,5 @@ require 'fir/version'
|
||||
require 'fir/cli'
|
||||
|
||||
module FIR
|
||||
CONFIG_PATH = "#{ENV['HOME']}/.fir-cli"
|
||||
API_YML_PATH = "#{File.dirname(__FILE__)}/fir/api.yml"
|
||||
APP_FILE_TYPE = %w(.ipa .apk).freeze
|
||||
|
||||
include Util
|
||||
|
||||
class << self
|
||||
attr_accessor :logger, :api, :config
|
||||
|
||||
def api
|
||||
@api ||= YAML.load_file(API_YML_PATH).symbolize_keys
|
||||
end
|
||||
|
||||
def config
|
||||
@config ||= YAML.load_file(CONFIG_PATH).symbolize_keys if File.exist?(CONFIG_PATH)
|
||||
end
|
||||
|
||||
def reload_config
|
||||
@config = YAML.load_file(CONFIG_PATH).symbolize_keys
|
||||
end
|
||||
|
||||
def write_config hash
|
||||
File.open(CONFIG_PATH, 'w+') { |f| f << YAML.dump(hash) }
|
||||
end
|
||||
|
||||
def current_token
|
||||
@token ||= config[:token] if config
|
||||
end
|
||||
|
||||
def get url, params = {}, timeout = 300
|
||||
begin
|
||||
res = ::RestClient::Request.execute(
|
||||
method: :get,
|
||||
url: url,
|
||||
timeout: timeout,
|
||||
headers: default_headers.merge(params: params)
|
||||
)
|
||||
rescue => e
|
||||
logger.error "#{e.class}\n#{e.message}"
|
||||
exit 1
|
||||
end
|
||||
|
||||
JSON.parse(res.body.force_encoding("UTF-8"), symbolize_names: true)
|
||||
end
|
||||
|
||||
def post url, query, timeout = 300
|
||||
begin
|
||||
res = ::RestClient::Request.execute(
|
||||
method: :post,
|
||||
url: url,
|
||||
payload: query,
|
||||
timeout: timeout,
|
||||
headers: default_headers
|
||||
)
|
||||
rescue => e
|
||||
logger.error "#{e.class}\n#{e.message}"
|
||||
exit 1
|
||||
end
|
||||
|
||||
JSON.parse(res.body.force_encoding("UTF-8"), symbolize_names: true)
|
||||
end
|
||||
|
||||
def patch url, query, timeout = 300
|
||||
begin
|
||||
res = ::RestClient::Request.execute(
|
||||
method: :patch,
|
||||
url: url,
|
||||
payload: query,
|
||||
timeout: timeout,
|
||||
headers: default_headers
|
||||
)
|
||||
rescue => e
|
||||
logger.error "#{e.class}\n#{e.message}"
|
||||
exit 1
|
||||
end
|
||||
|
||||
JSON.parse(res.body.force_encoding("UTF-8"), symbolize_names: true)
|
||||
end
|
||||
|
||||
def put url, query, timeout = 300
|
||||
begin
|
||||
res = ::RestClient::Request.execute(
|
||||
method: :put,
|
||||
url: url,
|
||||
payload: query,
|
||||
timeout: timeout,
|
||||
headers: default_headers
|
||||
)
|
||||
rescue => e
|
||||
logger.error "#{e.class}\n#{e.message}"
|
||||
exit 1
|
||||
end
|
||||
|
||||
JSON.parse(res.body.force_encoding("UTF-8"), symbolize_names: true)
|
||||
end
|
||||
|
||||
def default_headers
|
||||
{ content_type: :json, source: 'fir-cli', cli_version: FIR::VERSION }
|
||||
end
|
||||
|
||||
alias_method :☠, :exit
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
domain: 'http://fir.im'
|
||||
base_url: 'http://api.fir.im'
|
||||
user_url: 'http://api.fir.im/user'
|
||||
app_url: 'http://api.fir.im/apps'
|
||||
udids_url: 'http://api.fir.im/devices/multi_udid'
|
||||
fir:
|
||||
domain: 'http://fir.im'
|
||||
base_url: 'http://api.fir.im'
|
||||
user_url: 'http://api.fir.im/user'
|
||||
app_url: 'http://api.fir.im/apps'
|
||||
udids_url: 'http://api.fir.im/devices/multi_udid'
|
||||
bughd:
|
||||
domain: 'http://bughd.com'
|
||||
base_url: 'http://api.bughd.com'
|
||||
project_url: 'http://api.bughd.com/projects'
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
module FIR
|
||||
class CLI < Thor
|
||||
class_option :token, type: :string, aliases: "-T", desc: "User's api_token at FIR.im"
|
||||
class_option :token, type: :string, aliases: "-T", desc: "User's API Token at FIR.im"
|
||||
class_option :logfile, type: :string, aliases: "-L", desc: "Path to writable logfile"
|
||||
class_option :verbose, type: :boolean, aliases: "-V", desc: "Show verbose", default: true
|
||||
class_option :quiet, type: :boolean, aliases: "-q", desc: "Silence commands"
|
||||
@@ -16,9 +16,9 @@ module FIR
|
||||
|
||||
Example:
|
||||
|
||||
$ fir b <project dir> [-C <configuration>] [-t <target name>] [-o <ipa output dir>] [settings] [-c <changelog>] [-p -T <your token>]
|
||||
$ fir b <project dir> [-C <configuration>] [-t <target name>] [-o <ipa output dir>] [settings] [-c <changelog>] [-p -T <your api token>]
|
||||
|
||||
$ fir b <workspace dir> -w -S <scheme name> [-C <configuration>] [-t <target name>] [-o <ipa output dir>] [settings] [-c <changelog>] [-p -T <your token>]
|
||||
$ fir b <workspace dir> -w -S <scheme name> [-C <configuration>] [-t <target name>] [-o <ipa output dir>] [settings] [-c <changelog>] [-p -T <your api token>]
|
||||
LONGDESC
|
||||
map ["b", "build"] => :build_ipa
|
||||
method_option :workspace, type: :boolean, aliases: "-w", desc: "true/false if build workspace"
|
||||
@@ -46,6 +46,13 @@ module FIR
|
||||
end
|
||||
|
||||
desc "publish APP_FILE_PATH", "Publish iOS/Android app to FIR.im, support ipa/apk file (aliases: 'p')."
|
||||
long_desc <<-LONGDESC
|
||||
`publish` command will publish your app file to FIR.im, also the command support to publish app's short & changelog.
|
||||
|
||||
Example:
|
||||
|
||||
$ fir p <app file path> [-c <changelog> -s <custom short link>]
|
||||
LONGDESC
|
||||
map "p" => :publish
|
||||
method_option :short, type: :string, aliases: "-s", desc: "Set custom short link"
|
||||
method_option :changelog, type: :string, aliases: "-c", desc: "Set changelog"
|
||||
@@ -60,7 +67,7 @@ module FIR
|
||||
def login *args
|
||||
prepare :login
|
||||
|
||||
token = options[:token] || args.first || ask("Please enter your FIR.im token:", :white, echo: true)
|
||||
token = options[:token] || args.first || ask("Please enter your FIR.im API Token:", :white, echo: true)
|
||||
FIR.login(token)
|
||||
end
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# encoding: utf-8
|
||||
|
||||
require_relative './patches/concern'
|
||||
require_relative './patches/native_patch'
|
||||
require_relative './patches/os_patch'
|
||||
require_relative './patches/parser_patch'
|
||||
|
||||
Binary file not shown.
144
lib/fir/patches/concern.rb
Normal file
144
lib/fir/patches/concern.rb
Normal file
@@ -0,0 +1,144 @@
|
||||
# encoding: utf-8
|
||||
|
||||
module ActiveSupport
|
||||
# A typical module looks like this:
|
||||
#
|
||||
# module M
|
||||
# def self.included(base)
|
||||
# base.extend ClassMethods
|
||||
# base.class_eval do
|
||||
# scope :disabled, -> { where(disabled: true) }
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# module ClassMethods
|
||||
# ...
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# By using <tt>ActiveSupport::Concern</tt> the above module could instead be
|
||||
# written as:
|
||||
#
|
||||
# require 'active_support/concern'
|
||||
#
|
||||
# module M
|
||||
# extend ActiveSupport::Concern
|
||||
#
|
||||
# included do
|
||||
# scope :disabled, -> { where(disabled: true) }
|
||||
# end
|
||||
#
|
||||
# class_methods do
|
||||
# ...
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# Moreover, it gracefully handles module dependencies. Given a +Foo+ module
|
||||
# and a +Bar+ module which depends on the former, we would typically write the
|
||||
# following:
|
||||
#
|
||||
# module Foo
|
||||
# def self.included(base)
|
||||
# base.class_eval do
|
||||
# def self.method_injected_by_foo
|
||||
# ...
|
||||
# end
|
||||
# end
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# module Bar
|
||||
# def self.included(base)
|
||||
# base.method_injected_by_foo
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# class Host
|
||||
# include Foo # We need to include this dependency for Bar
|
||||
# include Bar # Bar is the module that Host really needs
|
||||
# end
|
||||
#
|
||||
# But why should +Host+ care about +Bar+'s dependencies, namely +Foo+? We
|
||||
# could try to hide these from +Host+ directly including +Foo+ in +Bar+:
|
||||
#
|
||||
# module Bar
|
||||
# include Foo
|
||||
# def self.included(base)
|
||||
# base.method_injected_by_foo
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# class Host
|
||||
# include Bar
|
||||
# end
|
||||
#
|
||||
# Unfortunately this won't work, since when +Foo+ is included, its <tt>base</tt>
|
||||
# is the +Bar+ module, not the +Host+ class. With <tt>ActiveSupport::Concern</tt>,
|
||||
# module dependencies are properly resolved:
|
||||
#
|
||||
# require 'active_support/concern'
|
||||
#
|
||||
# module Foo
|
||||
# extend ActiveSupport::Concern
|
||||
# included do
|
||||
# def self.method_injected_by_foo
|
||||
# ...
|
||||
# end
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# module Bar
|
||||
# extend ActiveSupport::Concern
|
||||
# include Foo
|
||||
#
|
||||
# included do
|
||||
# self.method_injected_by_foo
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# class Host
|
||||
# include Bar # It works, now Bar takes care of its dependencies
|
||||
# end
|
||||
module Concern
|
||||
class MultipleIncludedBlocks < StandardError #:nodoc:
|
||||
def initialize
|
||||
super "Cannot define multiple 'included' blocks for a Concern"
|
||||
end
|
||||
end
|
||||
|
||||
def self.extended(base) #:nodoc:
|
||||
base.instance_variable_set(:@_dependencies, [])
|
||||
end
|
||||
|
||||
def append_features(base)
|
||||
if base.instance_variable_defined?(:@_dependencies)
|
||||
base.instance_variable_get(:@_dependencies) << self
|
||||
return false
|
||||
else
|
||||
return false if base < self
|
||||
@_dependencies.each { |dep| base.send(:include, dep) }
|
||||
super
|
||||
base.extend const_get(:ClassMethods) if const_defined?(:ClassMethods)
|
||||
base.class_eval(&@_included_block) if instance_variable_defined?(:@_included_block)
|
||||
end
|
||||
end
|
||||
|
||||
def included(base = nil, &block)
|
||||
if base.nil?
|
||||
raise MultipleIncludedBlocks if instance_variable_defined?(:@_included_block)
|
||||
|
||||
@_included_block = block
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
def class_methods(&class_methods_module_definition)
|
||||
mod = const_defined?(:ClassMethods, false) ?
|
||||
const_get(:ClassMethods) :
|
||||
const_set(:ClassMethods, Module.new)
|
||||
|
||||
mod.module_eval(&class_methods_module_definition)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,169 +0,0 @@
|
||||
# encoding: utf-8
|
||||
|
||||
module Parser
|
||||
|
||||
class << self
|
||||
|
||||
def png_bin
|
||||
@png_bin ||= File.expand_path("../bin/pngcrush", __FILE__)
|
||||
end
|
||||
|
||||
def uncrush_icon crushed_icon_path, uncrushed_icon_path
|
||||
system("#{png_bin} -revert-iphone-optimizations #{crushed_icon_path} #{uncrushed_icon_path} &> /dev/null")
|
||||
end
|
||||
|
||||
def crush_icon uncrushed_icon_path, crushed_icon_path
|
||||
system("#{png_bin} -iphone #{uncrushed_icon_path} #{crushed_icon_path} &> /dev/null")
|
||||
end
|
||||
end
|
||||
|
||||
class IPA
|
||||
|
||||
def initialize(path)
|
||||
@path = path
|
||||
end
|
||||
|
||||
def app
|
||||
@app ||= App.new(app_path)
|
||||
end
|
||||
|
||||
def app_path
|
||||
@app_path ||= Dir.glob(File.join(contents, 'Payload', '*.app')).first
|
||||
end
|
||||
|
||||
def cleanup
|
||||
return unless @contents
|
||||
FileUtils.rm_rf(@contents)
|
||||
@contents = nil
|
||||
end
|
||||
|
||||
def metadata
|
||||
return unless has_metadata?
|
||||
@metadata ||= CFPropertyList.native_types(CFPropertyList::List.new(file: metadata_path).value)
|
||||
end
|
||||
|
||||
def has_metadata?
|
||||
File.file? metadata_path
|
||||
end
|
||||
|
||||
def metadata_path
|
||||
@metadata_path ||= File.join(@contents, 'iTunesMetadata.plist')
|
||||
end
|
||||
|
||||
def release_type
|
||||
has_metadata? ? 'store' : 'adhoc'
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def contents
|
||||
return if @contents
|
||||
@contents = "fir-cli_tmp/ipa_files-#{Time.now.to_i}"
|
||||
|
||||
Zip::File.open(@path) do |zip_file|
|
||||
zip_file.each do |f|
|
||||
f_path = File.join(@contents, f.name)
|
||||
FileUtils.mkdir_p(File.dirname(f_path))
|
||||
zip_file.extract(f, f_path) unless File.exist?(f_path)
|
||||
end
|
||||
end
|
||||
|
||||
@contents
|
||||
end
|
||||
end
|
||||
|
||||
class App
|
||||
|
||||
def initialize(path)
|
||||
@path = path
|
||||
end
|
||||
|
||||
def info
|
||||
@info ||= CFPropertyList.native_types(
|
||||
CFPropertyList::List.new(file: File.join(@path, 'Info.plist')).value)
|
||||
end
|
||||
|
||||
def name
|
||||
info['CFBundleName']
|
||||
end
|
||||
|
||||
def identifier
|
||||
info['CFBundleIdentifier']
|
||||
end
|
||||
|
||||
def display_name
|
||||
info['CFBundleDisplayName']
|
||||
end
|
||||
|
||||
def version
|
||||
info['CFBundleVersion']
|
||||
end
|
||||
|
||||
def short_version
|
||||
info['CFBundleShortVersionString']
|
||||
end
|
||||
|
||||
def icons
|
||||
@icons ||= begin
|
||||
icons = []
|
||||
info['CFBundleIcons']['CFBundlePrimaryIcon']['CFBundleIconFiles'].each do |name|
|
||||
icons << get_image(name)
|
||||
icons << get_image("#{name}@2x")
|
||||
end
|
||||
icons.delete_if { |i| !i }
|
||||
rescue NoMethodError
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
def mobileprovision
|
||||
return unless has_mobileprovision?
|
||||
return @mobileprovision if @mobileprovision
|
||||
|
||||
cmd = "security cms -D -i \"#{mobileprovision_path}\""
|
||||
begin
|
||||
@mobileprovision = CFPropertyList.native_types(CFPropertyList::List.new(data: `#{cmd}`).value)
|
||||
rescue CFFormatError
|
||||
@mobileprovision = {}
|
||||
end
|
||||
end
|
||||
|
||||
def has_mobileprovision?
|
||||
File.file? mobileprovision_path
|
||||
end
|
||||
|
||||
def mobileprovision_path
|
||||
@mobileprovision_path ||= File.join(@path, 'embedded.mobileprovision')
|
||||
end
|
||||
|
||||
def hide_developer_certificates
|
||||
mobileprovision.delete('DeveloperCertificates') if has_mobileprovision?
|
||||
end
|
||||
|
||||
def devices
|
||||
mobileprovision['ProvisionedDevices'] if has_mobileprovision?
|
||||
end
|
||||
|
||||
def distribution_name
|
||||
"#{mobileprovision['Name']} - #{mobileprovision['TeamName']}" if has_mobileprovision?
|
||||
end
|
||||
|
||||
def release_type
|
||||
if has_mobileprovision?
|
||||
if devices
|
||||
'adhoc'
|
||||
else
|
||||
'inhouse'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def get_image name
|
||||
path = File.join(@path, "#{name}.png")
|
||||
return nil unless File.exist?(path)
|
||||
path
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,5 +1,8 @@
|
||||
# encoding: utf-8
|
||||
|
||||
require_relative './util/http'
|
||||
require_relative './util/config'
|
||||
require_relative './util/parser'
|
||||
require_relative './util/login'
|
||||
require_relative './util/me'
|
||||
require_relative './util/info'
|
||||
@@ -8,20 +11,21 @@ require_relative './util/publish'
|
||||
|
||||
module FIR
|
||||
module Util
|
||||
|
||||
def self.included base
|
||||
base.extend ClassMethods
|
||||
base.extend Login
|
||||
base.extend Me
|
||||
base.extend Info
|
||||
base.extend Build
|
||||
base.extend Publish
|
||||
end
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
module ClassMethods
|
||||
include FIR::Http
|
||||
include FIR::Config
|
||||
include FIR::Login
|
||||
include FIR::Me
|
||||
include FIR::Info
|
||||
include FIR::Build
|
||||
include FIR::Publish
|
||||
|
||||
attr_accessor :logger
|
||||
|
||||
def fetch_user_info token
|
||||
get api[:user_url], api_token: token
|
||||
get fir_api[:user_url], api_token: token
|
||||
end
|
||||
|
||||
def check_supported_file path
|
||||
|
||||
@@ -4,66 +4,27 @@ module FIR
|
||||
module Build
|
||||
|
||||
def build_ipa *args, options
|
||||
# initialize build options
|
||||
# check build environment and make build cmd
|
||||
check_osx
|
||||
|
||||
if args.first.blank? || !File.exist?(args.first)
|
||||
@build_dir = Dir.pwd
|
||||
else
|
||||
@build_dir = File.absolute_path(args.shift.to_s) # pop the first param
|
||||
end
|
||||
|
||||
@build_cmd = "xcodebuild build -sdk iphoneos"
|
||||
@build_tmp_dir = Dir.mktmpdir
|
||||
@custom_settings = parse_custom_settings(args) # convert ['a=1', 'b=2'] => { 'a' => '1', 'b' => '2' }
|
||||
@configuration = options[:configuration]
|
||||
@wrapper_name = File.basename(options[:name].to_s, '.*') + '.app' unless options[:name].blank?
|
||||
@target_name = options[:target]
|
||||
@scheme_name = options[:scheme]
|
||||
@output_path = options[:output].blank? ? "#{@build_dir}/build_ipa" : File.absolute_path(options[:output].to_s)
|
||||
@dsym_name = @wrapper_name + '.dSYM' unless @wrapper_name.blank?
|
||||
@build_tmp_dir = Dir.mktmpdir
|
||||
@output_path = options[:output].blank? ? "#{@build_dir}/fir_build_ipa" : File.absolute_path(options[:output].to_s)
|
||||
@ipa_build_cmd = initialize_ipa_build_cmd(args, options)
|
||||
|
||||
# check build environment and make build cmd
|
||||
check_osx
|
||||
if options.workspace?
|
||||
@workspace = check_and_find_workspace(@build_dir)
|
||||
check_scheme(@scheme_name)
|
||||
@build_cmd += " -workspace '#{@workspace}' -scheme '#{@scheme_name}'"
|
||||
else
|
||||
@project = check_and_find_project(@build_dir)
|
||||
@build_cmd += " -project '#{@project}'"
|
||||
end
|
||||
|
||||
@build_cmd += " -configuration '#{@configuration}'" unless @configuration.blank?
|
||||
@build_cmd += " -target '#{@target_name}'" unless @target_name.blank?
|
||||
|
||||
# convert { "a" => "1", "b" => "2" } => "a='1' b='2'"
|
||||
@setting_str = @custom_settings.collect { |k, v| "#{k}='#{v}'" }.join(' ')
|
||||
@setting_str += " WRAPPER_NAME='#{@wrapper_name}'" unless @wrapper_name.blank?
|
||||
@setting_str += " TARGET_BUILD_DIR='#{@build_tmp_dir}'" unless @custom_settings['TARGET_BUILD_DIR']
|
||||
@setting_str += " CONFIGURATION_BUILD_DIR='#{@build_tmp_dir}'" unless @custom_settings['CONFIGURATION_BUILD_DIR']
|
||||
@setting_str += " DWARF_DSYM_FOLDER_PATH='#{@output_path}'" unless @custom_settings['DWARF_DSYM_FOLDER_PATH']
|
||||
@setting_str += " DWARF_DSYM_FILE_NAME='#{@dsym_name}'" unless @dsym_name.blank?
|
||||
|
||||
@build_cmd += " #{@setting_str} 2>&1"
|
||||
puts @build_cmd if $DEBUG
|
||||
puts @ipa_build_cmd if $DEBUG
|
||||
|
||||
logger.info "Building......"
|
||||
logger_info_dividing_line
|
||||
|
||||
logger.info `#{@build_cmd}`
|
||||
logger.info `#{@ipa_build_cmd}`
|
||||
|
||||
FileUtils.mkdir_p(@output_path) unless File.exist?(@output_path)
|
||||
Dir.chdir(@build_tmp_dir) do
|
||||
apps = Dir["*.app"]
|
||||
if apps.length == 0
|
||||
logger.error "Builded has no output app, Can not be packaged"
|
||||
exit 1
|
||||
end
|
||||
|
||||
apps.each do |app|
|
||||
ipa_path = File.join(@output_path, "#{File.basename(app, '.app')}.ipa")
|
||||
zip_app2ipa(File.join(@build_tmp_dir, app), ipa_path)
|
||||
end
|
||||
end
|
||||
output_ipa
|
||||
|
||||
logger.info "Build Success"
|
||||
|
||||
@@ -78,12 +39,68 @@ module FIR
|
||||
|
||||
private
|
||||
|
||||
def parse_custom_settings args
|
||||
def initialize_ipa_build_cmd args, options
|
||||
ipa_build_cmd = "xcodebuild build -sdk iphoneos"
|
||||
|
||||
@configuration = options[:configuration]
|
||||
@wrapper_name = File.basename(options[:name].to_s, '.*') + '.app' unless options[:name].blank?
|
||||
@target_name = options[:target]
|
||||
@scheme_name = options[:scheme]
|
||||
@dsym_name = @wrapper_name + '.dSYM' unless @wrapper_name.blank?
|
||||
|
||||
if options.workspace?
|
||||
workspace = check_and_find_ios_workspace(@build_dir)
|
||||
check_ios_scheme(@scheme_name)
|
||||
ipa_build_cmd += " -workspace '#{workspace}' -scheme '#{@scheme_name}'"
|
||||
else
|
||||
project = check_and_find_ios_project(@build_dir)
|
||||
ipa_build_cmd += " -project '#{project}'"
|
||||
end
|
||||
|
||||
ipa_build_cmd += " -configuration '#{@configuration}'" unless @configuration.blank?
|
||||
ipa_build_cmd += " -target '#{@target_name}'" unless @target_name.blank?
|
||||
ipa_build_cmd += " #{ipa_custom_settings(args)} 2>&1"
|
||||
|
||||
ipa_build_cmd
|
||||
end
|
||||
|
||||
def ipa_custom_settings args
|
||||
custom_settings = parse_ipa_custom_settings(args)
|
||||
|
||||
# convert { "a" => "1", "b" => "2" } => "a='1' b='2'"
|
||||
setting_str = custom_settings.collect { |k, v| "#{k}='#{v}'" }.join(' ')
|
||||
setting_str += " WRAPPER_NAME='#{@wrapper_name}'" unless @wrapper_name.blank?
|
||||
setting_str += " TARGET_BUILD_DIR='#{@build_tmp_dir}'" unless custom_settings['TARGET_BUILD_DIR']
|
||||
setting_str += " CONFIGURATION_BUILD_DIR='#{@build_tmp_dir}'" unless custom_settings['CONFIGURATION_BUILD_DIR']
|
||||
setting_str += " DWARF_DSYM_FOLDER_PATH='#{@output_path}'" unless custom_settings['DWARF_DSYM_FOLDER_PATH']
|
||||
setting_str += " DWARF_DSYM_FILE_NAME='#{@dsym_name}'" unless @dsym_name.blank?
|
||||
setting_str
|
||||
end
|
||||
|
||||
def output_ipa
|
||||
FileUtils.mkdir_p(@output_path) unless File.exist?(@output_path)
|
||||
Dir.chdir(@build_tmp_dir) do
|
||||
apps = Dir["*.app"]
|
||||
if apps.length == 0
|
||||
logger.error "Builded has no output app, Can not be packaged"
|
||||
exit 1
|
||||
end
|
||||
|
||||
apps.each do |app|
|
||||
ipa_path = File.join(@output_path, "#{File.basename(app, '.app')}.ipa")
|
||||
zip_app2ipa(File.join(@build_tmp_dir, app), ipa_path)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# convert ['a=1', 'b=2'] => { 'a' => '1', 'b' => '2' }
|
||||
def parse_ipa_custom_settings args
|
||||
hash = {}
|
||||
args.each do |setting|
|
||||
k, v = setting.split('=', 2).map(&:strip)
|
||||
hash[k] = v
|
||||
end
|
||||
|
||||
hash
|
||||
end
|
||||
|
||||
@@ -94,13 +111,13 @@ module FIR
|
||||
end
|
||||
end
|
||||
|
||||
def check_and_find_project path
|
||||
def check_and_find_ios_project path
|
||||
unless File.exist?(path)
|
||||
logger.error "The first param BUILD_DIR must be a xcodeproj directory"
|
||||
exit 1
|
||||
end
|
||||
|
||||
if is_project?(path)
|
||||
if is_ios_project?(path)
|
||||
project = path
|
||||
else
|
||||
project = Dir["#{path}/*.xcodeproj"].first
|
||||
@@ -113,13 +130,13 @@ module FIR
|
||||
project
|
||||
end
|
||||
|
||||
def check_and_find_workspace path
|
||||
def check_and_find_ios_workspace path
|
||||
unless File.exist?(path)
|
||||
logger.error "The first param BUILD_DIR must be a xcworkspace directory"
|
||||
exit 1
|
||||
end
|
||||
|
||||
if is_workspace?(path)
|
||||
if is_ios_workspace?(path)
|
||||
workspace = path
|
||||
else
|
||||
workspace = Dir["#{path}/*.xcworkspace"].first
|
||||
@@ -132,18 +149,18 @@ module FIR
|
||||
workspace
|
||||
end
|
||||
|
||||
def check_scheme scheme_name
|
||||
def check_ios_scheme scheme_name
|
||||
if scheme_name.blank?
|
||||
logger.error "Must provide a scheme by `-S` option when build a workspace"
|
||||
exit 1
|
||||
end
|
||||
end
|
||||
|
||||
def is_project? path
|
||||
def is_ios_project? path
|
||||
File.extname(path) == '.xcodeproj'
|
||||
end
|
||||
|
||||
def is_workspace? path
|
||||
def is_ios_workspace? path
|
||||
File.extname(path) == '.xcworkspace'
|
||||
end
|
||||
|
||||
|
||||
35
lib/fir/util/config.rb
Normal file
35
lib/fir/util/config.rb
Normal file
@@ -0,0 +1,35 @@
|
||||
# encoding: utf-8
|
||||
|
||||
module FIR
|
||||
module Config
|
||||
CONFIG_PATH = "#{ENV['HOME']}/.fir-cli"
|
||||
API_YML_PATH = File.expand_path("../../", __FILE__) + '/api.yml'
|
||||
APP_FILE_TYPE = %w(.ipa .apk).freeze
|
||||
|
||||
def fir_api
|
||||
@fir_api ||= YAML.load_file(API_YML_PATH).deep_symbolize_keys[:fir]
|
||||
end
|
||||
|
||||
def bughd_api
|
||||
@bughd_api ||= YAML.load_file(API_YML_PATH).deep_symbolize_keys[:bughd]
|
||||
end
|
||||
|
||||
def config
|
||||
@config ||= YAML.load_file(CONFIG_PATH).deep_symbolize_keys if File.exist?(CONFIG_PATH)
|
||||
end
|
||||
|
||||
def reload_config
|
||||
@config = YAML.load_file(CONFIG_PATH).deep_symbolize_keys
|
||||
end
|
||||
|
||||
def write_config hash
|
||||
File.open(CONFIG_PATH, 'w+') { |f| f << YAML.dump(hash) }
|
||||
end
|
||||
|
||||
def current_token
|
||||
@token ||= config[:token] if config
|
||||
end
|
||||
|
||||
alias_method :☠, :exit
|
||||
end
|
||||
end
|
||||
79
lib/fir/util/http.rb
Normal file
79
lib/fir/util/http.rb
Normal file
@@ -0,0 +1,79 @@
|
||||
# encoding: utf-8
|
||||
|
||||
module FIR
|
||||
module Http
|
||||
|
||||
def get url, params = {}, timeout = 300
|
||||
begin
|
||||
res = ::RestClient::Request.execute(
|
||||
method: :get,
|
||||
url: url,
|
||||
timeout: timeout,
|
||||
headers: default_headers.merge(params: params)
|
||||
)
|
||||
rescue => e
|
||||
logger.error "#{e.class}\n#{e.message}"
|
||||
exit 1
|
||||
end
|
||||
|
||||
JSON.parse(res.body.force_encoding("UTF-8"), symbolize_names: true)
|
||||
end
|
||||
|
||||
def post url, query, timeout = 300
|
||||
begin
|
||||
res = ::RestClient::Request.execute(
|
||||
method: :post,
|
||||
url: url,
|
||||
payload: query,
|
||||
timeout: timeout,
|
||||
headers: default_headers
|
||||
)
|
||||
rescue => e
|
||||
logger.error "#{e.class}\n#{e.message}"
|
||||
exit 1
|
||||
end
|
||||
|
||||
JSON.parse(res.body.force_encoding("UTF-8"), symbolize_names: true)
|
||||
end
|
||||
|
||||
def patch url, query, timeout = 300
|
||||
begin
|
||||
res = ::RestClient::Request.execute(
|
||||
method: :patch,
|
||||
url: url,
|
||||
payload: query,
|
||||
timeout: timeout,
|
||||
headers: default_headers
|
||||
)
|
||||
rescue => e
|
||||
logger.error "#{e.class}\n#{e.message}"
|
||||
exit 1
|
||||
end
|
||||
|
||||
JSON.parse(res.body.force_encoding("UTF-8"), symbolize_names: true)
|
||||
end
|
||||
|
||||
def put url, query, timeout = 300
|
||||
begin
|
||||
res = ::RestClient::Request.execute(
|
||||
method: :put,
|
||||
url: url,
|
||||
payload: query,
|
||||
timeout: timeout,
|
||||
headers: default_headers
|
||||
)
|
||||
rescue => e
|
||||
logger.error "#{e.class}\n#{e.message}"
|
||||
exit 1
|
||||
end
|
||||
|
||||
JSON.parse(res.body.force_encoding("UTF-8"), symbolize_names: true)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def default_headers
|
||||
{ content_type: :json, source: 'fir-cli', cli_version: FIR::VERSION }
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -19,7 +19,7 @@ module FIR
|
||||
end
|
||||
|
||||
def ipa_info ipa_path, is_all
|
||||
ipa = Parser::IPA.new(ipa_path)
|
||||
ipa = FIR::Parser::Ipa.new(ipa_path)
|
||||
app = ipa.app
|
||||
|
||||
info = {
|
||||
|
||||
157
lib/fir/util/parser.rb
Normal file
157
lib/fir/util/parser.rb
Normal file
@@ -0,0 +1,157 @@
|
||||
# encoding: utf-8
|
||||
|
||||
module FIR
|
||||
module Parser
|
||||
|
||||
class Ipa
|
||||
|
||||
def initialize(path)
|
||||
@path = path
|
||||
end
|
||||
|
||||
def app
|
||||
@app ||= App.new(app_path)
|
||||
end
|
||||
|
||||
def app_path
|
||||
@app_path ||= Dir.glob(File.join(contents, 'Payload', '*.app')).first
|
||||
end
|
||||
|
||||
def cleanup
|
||||
return unless @contents
|
||||
FileUtils.rm_rf(@contents)
|
||||
@contents = nil
|
||||
end
|
||||
|
||||
def metadata
|
||||
return unless has_metadata?
|
||||
@metadata ||= CFPropertyList.native_types(CFPropertyList::List.new(file: metadata_path).value)
|
||||
end
|
||||
|
||||
def has_metadata?
|
||||
File.file? metadata_path
|
||||
end
|
||||
|
||||
def metadata_path
|
||||
@metadata_path ||= File.join(@contents, 'iTunesMetadata.plist')
|
||||
end
|
||||
|
||||
def release_type
|
||||
has_metadata? ? 'store' : 'adhoc'
|
||||
end
|
||||
|
||||
def contents
|
||||
return if @contents
|
||||
@contents = "fir-cli_tmp/ipa_files-#{Time.now.to_i}"
|
||||
|
||||
Zip::File.open(@path) do |zip_file|
|
||||
zip_file.each do |f|
|
||||
f_path = File.join(@contents, f.name)
|
||||
FileUtils.mkdir_p(File.dirname(f_path))
|
||||
zip_file.extract(f, f_path) unless File.exist?(f_path)
|
||||
end
|
||||
end
|
||||
|
||||
@contents
|
||||
end
|
||||
|
||||
class App
|
||||
|
||||
def initialize(path)
|
||||
@path = path
|
||||
end
|
||||
|
||||
def info
|
||||
@info ||= CFPropertyList.native_types(
|
||||
CFPropertyList::List.new(file: File.join(@path, 'Info.plist')).value)
|
||||
end
|
||||
|
||||
def name
|
||||
info['CFBundleName']
|
||||
end
|
||||
|
||||
def identifier
|
||||
info['CFBundleIdentifier']
|
||||
end
|
||||
|
||||
def display_name
|
||||
info['CFBundleDisplayName']
|
||||
end
|
||||
|
||||
def version
|
||||
info['CFBundleVersion']
|
||||
end
|
||||
|
||||
def short_version
|
||||
info['CFBundleShortVersionString']
|
||||
end
|
||||
|
||||
def icons
|
||||
@icons ||= begin
|
||||
icons = []
|
||||
info['CFBundleIcons']['CFBundlePrimaryIcon']['CFBundleIconFiles'].each do |name|
|
||||
icons << get_image(name)
|
||||
icons << get_image("#{name}@2x")
|
||||
end
|
||||
icons.delete_if { |i| !i }
|
||||
rescue NoMethodError
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
def mobileprovision
|
||||
return unless has_mobileprovision?
|
||||
return @mobileprovision if @mobileprovision
|
||||
|
||||
cmd = "security cms -D -i \"#{mobileprovision_path}\""
|
||||
begin
|
||||
@mobileprovision = CFPropertyList.native_types(CFPropertyList::List.new(data: `#{cmd}`).value)
|
||||
rescue CFFormatError
|
||||
@mobileprovision = {}
|
||||
end
|
||||
end
|
||||
|
||||
def has_mobileprovision?
|
||||
File.file? mobileprovision_path
|
||||
end
|
||||
|
||||
def mobileprovision_path
|
||||
@mobileprovision_path ||= File.join(@path, 'embedded.mobileprovision')
|
||||
end
|
||||
|
||||
def hide_developer_certificates
|
||||
mobileprovision.delete('DeveloperCertificates') if has_mobileprovision?
|
||||
end
|
||||
|
||||
def devices
|
||||
mobileprovision['ProvisionedDevices'] if has_mobileprovision?
|
||||
end
|
||||
|
||||
def distribution_name
|
||||
"#{mobileprovision['Name']} - #{mobileprovision['TeamName']}" if has_mobileprovision?
|
||||
end
|
||||
|
||||
def release_type
|
||||
if has_mobileprovision?
|
||||
if devices
|
||||
'adhoc'
|
||||
else
|
||||
'inhouse'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def get_image name
|
||||
path = File.join(@path, "#{name}.png")
|
||||
return nil unless File.exist?(path)
|
||||
path
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class Apk
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -8,8 +8,8 @@ module FIR
|
||||
token = options[:token] || current_token
|
||||
changelog = options[:changelog].to_s
|
||||
|
||||
check_supported_file file_path
|
||||
check_token_cannot_be_blank token
|
||||
check_supported_file(file_path)
|
||||
check_token_cannot_be_blank(token)
|
||||
fetch_user_info(token)
|
||||
|
||||
logger.info "Publishing app......."
|
||||
@@ -47,7 +47,7 @@ module FIR
|
||||
published_app_info = fetch_app_info(app_id, api_token: token)
|
||||
|
||||
logger_info_dividing_line
|
||||
logger.info "Published succeed: #{api[:domain]}/#{published_app_info[:short]}"
|
||||
logger.info "Published succeed: #{fir_api[:domain]}/#{published_app_info[:short]}"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -81,23 +81,23 @@ module FIR
|
||||
|
||||
def upload_device_info hash
|
||||
logger.info "Updating devices info......"
|
||||
post api[:udids_url], hash
|
||||
post fir_api[:udids_url], hash
|
||||
end
|
||||
|
||||
def update_app_info id, hash
|
||||
logger.info "Updating app info......"
|
||||
patch api[:app_url] + "/#{id}", hash
|
||||
patch fir_api[:app_url] + "/#{id}", hash
|
||||
end
|
||||
|
||||
def fetch_uploading_info hash
|
||||
logger.info "Fetching #{@app_info[:identifier]}@FIR.im uploading info......"
|
||||
|
||||
post api[:app_url], hash
|
||||
post fir_api[:app_url], hash
|
||||
end
|
||||
|
||||
def fetch_app_info id, hash
|
||||
logger.info "Fetch app info from FIR.im"
|
||||
get api[:app_url] + "/#{id}", hash
|
||||
get fir_api[:app_url] + "/#{id}", hash
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -9,5 +9,6 @@ class PublishTest < Minitest::Test
|
||||
}
|
||||
|
||||
assert FIR.publish(default_ipa, options)
|
||||
assert FIR.publish(default_apk, options)
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user