Initial commit

This commit is contained in:
Yan
2014-11-26 02:30:11 +08:00
commit b1c2e1d679
17 changed files with 742 additions and 0 deletions

85
.gitignore vendored Normal file
View File

@@ -0,0 +1,85 @@
# Created by https://www.gitignore.io
### Ruby ###
*.gem
*.rbc
/.config
/coverage/
/InstalledFiles
/pkg/
/spec/reports/
/test/tmp/
/test/version_tmp/
/tmp/
## Specific to RubyMotion:
.dat*
.repl_history
build/
## Documentation cache and generated files:
/.yardoc/
/_yardoc/
/doc/
/rdoc/
## Environment normalisation:
/.bundle/
/lib/bundler/man/
# for a library or gem, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# Gemfile.lock
# .ruby-version
# .ruby-gemset
# unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
.rvmrc
### Linux ###
*~
# KDE directory preferences
.directory
### OSX ###
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear on external disk
.Spotlight-V100
.Trashes
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
### SublimeText ###
# cache files for sublime text
*.tmlanguage.cache
*.tmPreferences.cache
*.stTheme.cache
# workspace files are user-specific
*.sublime-workspace
# project files should be checked into the repository, unless a significant
# proportion of contributors will probably not be using SublimeText
# *.sublime-project
# sftp configuration file
sftp-config.json

2
Gemfile Normal file
View File

@@ -0,0 +1,2 @@
source 'https://ruby.taobao.org/'
gemspec

37
Gemfile.lock Normal file
View File

@@ -0,0 +1,37 @@
PATH
remote: .
specs:
fir-cli (0.1.0)
lagunitas (= 0.0.1)
paint (~> 0.9.0)
pngdefry (= 0.1.1)
rest_client (~> 1.8.2)
ruby_apk (~> 0.7.1)
thor (~> 0.19.1)
user_config (= 0.0.4)
GEM
remote: https://ruby.taobao.org/
specs:
CFPropertyList (2.2.8)
lagunitas (0.0.1)
CFPropertyList
pngdefry (>= 0.1.0)
zip
netrc (0.7.9)
paint (0.9.0)
pngdefry (0.1.1)
rest_client (1.8.2)
netrc (~> 0.7.7)
ruby_apk (0.7.1)
rubyzip (< 1.0.0)
rubyzip (0.9.9)
thor (0.19.1)
user_config (0.0.4)
zip (2.0.2)
PLATFORMS
ruby
DEPENDENCIES
fir-cli!

22
LICENSE.txt Normal file
View File

@@ -0,0 +1,22 @@
Copyright (c) 2014 idy0013@gmail.com
MIT License
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

69
README.md Normal file
View File

@@ -0,0 +1,69 @@
FIR.im 命令行工具
---
###安装
```shell
gem install fir-cli
```
###指令文档
####帮助
> 以下指令可以获取帮助
```shell
fir help
```
> 以下指令获取具体指令的帮助
- `COMMAND`:具体的一个指令,如`publish``update`
```shell
fir help COMMAND
```
####登录
> 以下指令通过用户`USER_TOKEN`登录,已登录用户可以在[这里](http://fir.im/user/info)上可以查看自己的令牌。
```shell
fir login USER_TOKEN
```
####获取 ipa 文件的信息
> 以下指令可以显示 ipa 文件的信息。
- `-v`:可选,显示更多信息
- `-f`:可选,显示托管在 [fir.im](http://fir.im) 的信息
```shell
fir info IPA_FILE [-f] [-v]
```
####设置全局信息
> 以下指令可以设置全局信息.
- `-r`:可选,默认发布都会使用企业证书签名
- `-t TOKEN`:可选,设置登录用户的令牌(作用和`fir loging USER_TOKEN`一样)
- `-e EMAIL`:可选,设置用户使用企业签名服务的默认邮件地址
```shell
fir config [-r] [-t TOKEN] [-e EMAIL]
```
####企业证书签名
> 以下指令使用 [resign.tapbeta.com](http://resign.tapbeta.com) 进行企业证书签名
- `INPUT_IPA_FILE`:待签名的 ipa 文件
- `OUTPUT_IPA_FILE`:签名后的输出文件
- `-e EMAIL`:可选,设置使用签名服务的邮件地址,不填则使用全局设置
```shell
fir resign INTPUT_IPA_FILE OUTPUT_IPA_FILE [-e EMAIL]
```
####发布应用至 [fir.im](http://fir.im)
> 以下指令将指定 ipa 文件发布到 [fir.im](http://fir.im)
- `-r`:可选,设置此开关将首先企业签名,之后发布到 [fir.im](http://fir.im)
- `-s SHORT`:可选,指定发布应用的短地址
- `-t USER_TOKEN`:可选,设定发布应用的帐号,未设置则使用全局设置
- `-c CHANGE_LOG`:可选,设置发布的发布日志
```shell
fir publish IPA_FILE [-r] [-s SHORT] [-t USER_TOKEN] [-c CHANGE_LOG]
```

11
bin/fir Executable file
View File

@@ -0,0 +1,11 @@
#!/usr/bin/env ruby
require 'fir-cli'
Fir::Cli.load_extends
# begin
Fir::Cli.start ARGV
# rescue Exception => e
# puts "! 错误[#{ e.errno }]: #{ Paint[e.to_s, :red] }"
# end

25
fir-cli.gemspec Normal file
View File

@@ -0,0 +1,25 @@
# coding: utf-8
lib = File.expand_path('../lib', __FILE__)
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
Gem::Specification.new do |s|
s.name = 'fir-cli'
s.version = '0.1.0'
s.date = '2014-11-20'
s.summary = 'FIR.im 命令行工具'
s.description = 'FIR.im 命令行工具,支持 ios 和 android'
s.homepage = 'http://blog.fir.im/2014/fir-cli'
s.author = 'FIR.im'
s.email = 'fir-cli@fir.im'
s.license = 'GPL'
s.executables << 'fir'
s.files = `git ls-files -z`.split("\x0")
s.add_dependency 'lagunitas', '0.0.1'
s.add_dependency 'user_config', '0.0.4'
s.add_dependency 'pngdefry', '0.1.1'
s.add_dependency 'rest_client', '~> 1.8.2'
s.add_dependency 'paint', '~> 0.9.0'
s.add_dependency 'thor', '~> 0.19.1'
s.add_dependency 'ruby_apk', '~> 0.7.1'
end

8
fir-cli.rb Normal file
View File

@@ -0,0 +1,8 @@
require 'thor'
module Fir
class Cli < Thor
end
end
Dir[File.dirname(__FILE__) + '/lib/*.rb'].each {|f| require f }
Dir[File.dirname(__FILE__) + '/lib/fir-cli-commands/*.rb'].each {|f| require f }

View File

@@ -0,0 +1,21 @@
module Fir
class Cli < Thor
desc 'config', '配置全局设置'
option :token, :aliases => '-t', :desc => '用户令牌'
option :email, :aliases => '-e', :desc => '邮件地址'
option :resign, :aliases => '-r', :desc => '是否以企业签名发布 ios 应用', :type => :boolean
option :verbose, :aliases => '-v', :desc => '输出级别项 v, vv, vvv', :type => :string, :enum => ['v', 'vv', 'vvv']
option :quiet, :aliases => '-q', :desc => '安静模式', :type => :boolean
def config
if options.length > 0
options.each do |conf|
puts "> #{ Paint[conf[0].to_s.rjust(10), :blue] } : #{ @config[conf[0].to_s] } => #{ conf[1] }"
@config[conf[0]] = conf[1]
end
@config.save
end
puts '> 当前设置:'
@config.each {|conf| puts "> #{ Paint[conf[0].to_s.rjust(10), :blue] } => #{ conf[1] }"}
end
end
end

View File

@@ -0,0 +1,17 @@
module Fir
class Cli < Thor
desc 'info IPA_FILE_PATH', '获取 .ipa 文件的信息'
option :all, :aliases => '-a', :desc => '显示全部应用信息', :type => :boolean
option :fir, :aliases => '-f', :desc => '显示托管在 fir.im 的应用信息', :type => :boolean
option :verbose, :aliases => '-v', :desc => '设置输出级别 v, vv, vvv'
option :quiet, :aliases => '-q', :desc => '安静模式,不输出任何选项'
def info(path)
app = _info path, options[:all]
app.each { |i| puts "#{ Paint[i[0].to_s.rjust(18), :blue] } #{ i[1] }" }
if options[:fir]
fir_app = _fir_info app[:identifier], app[:type]
fir_app.each { |i| puts "#{ Paint[i[0].to_s.rjust(18), :blue] } #{ i[1] }" }
end
end
end
end

View File

@@ -0,0 +1,16 @@
module Fir
class Cli < Thor
desc 'login USER_TOKEN', '以 USER_TOKEN 身份登陆'
option :verbose, :aliases => '-v', :desc => '设置输出级别 v, vv, vvv'
option :quite, :aliases => '-q', :desc => '安静模式,不输出任何选项'
def login(token)
if _opt_token && _opt_token != token
_puts "> 已登陆用户: #{ _opt_token }"
_puts "> 替换为用户: #{ token }"
end
@config['token'] = token
@config.save
_puts "> 当前登陆用户为:#{ token }"
end
end
end

View File

@@ -0,0 +1,66 @@
module Fir
class Cli < Thor
desc 'publish IPA_FILE_PATH', '将 .ipa 文件发布至 FIR.im'
option :resign, :aliases => '-r', :desc => '进行企业签名', :type => :boolean
option :short, :aliases => '-s', :desc => '自定义短地址'
option :token, :aliases => '-t', :desc => '用户令牌,不写则使用已经登陆信息'
option :changelog, :aliases => '-c', :desc => '修改纪录,默认为空字符串', :default => ''
option :verbose, :aliases => '-v', :desc => '设置输出级别 v, vv, vvv'
option :quiet, :aliases => '-q', :desc => '安静模式,不输出任何选项'
def publish(path)
if _opt_resign && _is_ipa(path)
tfile = Tempfile.new ["resign-#{ SecureRandom.hex }", '.ipa']
resign path, tfile.path
path = tfile.path
end
app = _info path, true
fir_app = _fir_info app[:identifier], app[:type]
id = fir_app[:id]
short = fir_app[:short]
bundle_app = fir_app[:bundle][:pkg]
bundle_icon = fir_app[:bundle][:icon]
if app[:icons] != nil && app[:icons].length > 0
icon_path = app[:icons][0][:path]
if _is_ipa path
_puts '> 转换图标...'
Pngdefry.defry icon_path, icon_path
end
_puts "> 上传图标..."
RestClient.post bundle_icon[:url],
:key => bundle_icon[:key],
:token => bundle_icon[:token],
:file => File.new(icon_path, 'rb')
_puts '> 上传图标成功'
end
_puts '> 上传应用...'
res = RestClient.post bundle_app[:url],
:key => bundle_app[:key],
:token => bundle_app[:token],
:file => File.new(path, 'rb')
_puts '> 上传应用成功'
upload_res = JSON.parse res.body, :symbolize_names => true
_fir_put fir_app[:id],
:name => app[:display_name] || app[:name],
:short => options[:short] || fir_app[:short],
:version => app[:version],
:versionShort => app[:short_version]
_fir_vput upload_res[:versionOid],
:version => app[:version],
:versionShort => app[:short_version],
:devices => app[:devices],
:release_type => app[:release_type],
:changelog => options[:changelog]
_puts "> http://fir.im/#{short}"
end
end
end

View File

@@ -0,0 +1,67 @@
module Fir
class Cli < Thor
desc 'resign IPA_FILE_PATH OUTPUT_PATH', '使用 resign.tapbeta.com 进行企业签名'
option :email, :aliases => '-e', :desc => '邮件地址'
option :verbose, :aliases => '-v', :desc => '设置输出级别 v, vv, vvv'
option :quiet, :aliases => '-q', :desc => '安静模式,不输出任何选项'
def resign(ipath, opath)
_puts "! #{ Paint['resign.tapbeta.com 签名服务风险提示', :red] }"
_puts '! 无法保证签名证书的长期有效性,当某种条件满足后'
_puts '! 苹果会禁止该企业账号,所有由该企业账号所签发的'
_puts '! 证书都会失效。您如果使用该网站提供的服务进行应'
_puts "! 用分发,请注意:#{ Paint['当证书失效后,所有安装了已失效', :red ] }"
_puts "! #{ Paint['证书签名的用户都会无法正常运行应用;同时托管在', :red ] }"
_puts "! #{ Paint['fir.im 的应用将无法正常安装。', :red ] }"
if _opt_email == nil
_puts "! #{ Paint['您需要提供邮件地址才能使用 resign.tapbeta.com', :red] }"
_puts "! #{ Paint['的签名服务, 请输入:', :red] } fir config --email=EMAIL"
_puts "! #{ Paint['进行设置', :red] }"
exit 1
end
if !/\.ipa$/.match ipath
_puts Paint['! 只能给以 ipa 为扩展名的文件签名', :red]
exit 1
end
_puts '> 正在申请上传令牌...'
upload_res = RestClient.post 'http://api.resign.tapbeta.com/public/upload',
:email => @email,
:file => File.basename(ipath)
form = JSON.parse upload_res.body, :symbolize_names => true
tapbeta = {}
form.each do |f|
if /^tb_/.match f[0]
tapbeta[f[0]] = f[1]
form.delete f[0]
end
end
form[:file] = File.new Pathname.new(Dir.pwd).join(ipath).cleanpath, 'rb'
_puts '> 正在上传...'
res = RestClient.post tapbeta[:tb_upload_url], form
# Upyun's notify is fucking, handle it specific
# if tapbeta[:tb_upload_url].include? 'upyun'
# RestClient.get "#{ form[:'notify-url'] }?#{ URI.encode_www_form JSON.parse res.body }"
# end
_puts '> 正在排队...'
nped = true
info = {}
loop do
res = RestClient.get "http://api.resign.tapbeta.com/public/#{ tapbeta[:tb_upload_key] }",
:params => { :__mr => 'eyJ1cmwiOiIkKHVybCkiLCAicmVzaWduU3RhdHVzIjogIiQocmVzaWduU3RhdHVzKSJ9' }
info = JSON.parse res.body, :symbolize_names => true
if nped && info[:resignStatus] == 'doing'
_puts '> 正在签名...'
nped = false
end
break if info[:url] != ''
sleep 5
end
opath = Pathname.new(Dir.pwd).join(opath).cleanpath
_puts "> 正在下载到 #{ opath }..."
`curl #{ info[:url] } -o #{ opath } -s`
end
end
end

View File

@@ -0,0 +1,28 @@
module Fir
class Cli < Thor
desc 'update IPA_FILE_PATH|BUNDLE_ID', '更新 fir.im 的应用信息'
option :short, :aliases => '-s', :desc => '自定义短地址'
option :token, :aliases => '-t', :desc => '用户令牌,不写则使用已经登陆信息'
option :verbose, :aliases => '-v', :desc => '设置输出级别 v, vv, vvv'
option :quiet, :aliases => '-q', :desc => '安静模式,不输出任何选项'
def update(path)
identifier = path
identifier = _info(path)[:identifier] if File.exist? _path path
fir_app = _fir_info identifier
opt_short = options[:short]
post = {}
post[:short] = opt_short if opt_short
post.each { |i| post.delete i[0] if i[1] == fir_app[i[0]] }
if post.length == 0 then _puts '> 没有可以更新的项目'; return end
_fir_put fir_app[:id], post
fir_app = _fir_info identifier
if opt_short && opt_short != fir_app[:short]
_puts "> 短地址 #{ opt_short } 被占用FIR.im 自动更新短地址为 #{ fir_app[:short] }"
end
_puts "> http://fir.im/#{ fir_app[:short] }"
end
end
end

172
lib/fir-cli.ext.rb Normal file
View File

@@ -0,0 +1,172 @@
require 'uri'
require 'json'
require 'pathname'
require 'tempfile'
require 'securerandom'
require 'thor'
require 'paint'
require 'pngdefry'
require 'user_config'
require 'rest_client'
require 'lagunitas'
require 'ruby_apk'
module Fir
class Cli < Thor
def initialize(*args)
super
_init_config
_puts_welcome
end
def self.load_extends
`gem list --local`.each_line("\n") do |gem|
gem_name = /^[^\s]+/.match(gem)[0]
require gem_name if gem_name.start_with? 'fir-cli-'
end
end
private
def _init_config
@uconfig = UserConfig.new '.fir'
@config = @uconfig['default.yaml']
@tmpfiles = []
end
def _path(path)
path = Pathname.new(Dir.pwd).join(path).cleanpath
end
def _opt_token
@token ||= options[:token] || @config['token']
end
def _opt_email
@email ||= options[:email] || @config['email']
end
def _opt_verbose
@verbose ||= options[:verbose] || @config['verbose']
end
def _opt_resign
return options[:resign] if defined? options[:resign]
@resign ||= options[:resign] || @config['resign']
end
def _opt_quiet
return options[:quiet] if defined? options[:quiet]
@quiet ||= options[:quiet] || @config['quiet']
end
def _is_ipa(path)
path.end_with? '.ipa'
end
def _is_apk(path)
path.end_with? '.apk'
end
def _is_identifier(str)
/^(?:(?:[a-zA-Z]|[a-zA-Z][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*(?:[A-Za-z]|[A-Za-z][A-Za-z0-9\-]*[A-Za-z0-9])$/.match str
end
def _chk_login
if !_opt_token
_puts "! #{ Paint['您还没有登录, 请输入:', :red] } fir login USER_TOKEN #{ Paint['进行登录', :red] }"
exit 1
end
end
def _puts_welcome
_puts "> #{ Paint['欢迎使用 FIR.im 命令行工具,如需帮助请输入:', :green] } fir help"
end
def _puts(text)
return if _opt_quiet
case _opt_verbose || 'vv' # If nothing about log is set, use the default option - vv
when 'v'
puts text if text.start_with?('!')
when 'vv'
puts text if text.start_with?('!') || text.start_with?('>')
when 'vvv'
puts text if text.start_with?('!') || text.start_with?('>') || text.start_with?('-')
end
end
def _info(path, more = false)
if _is_ipa path
_ipa_info path, more
elsif _is_apk path
_apk_info path, more
else
_puts "! #{ Paint['只能支持后缀为 ipa 和 apk 地文件', :red] }"
end
end
def _apk_info(path, more = false)
path = _path path
apk = Android::Apk.new path
info = {
:type => 'android',
:identifier => apk.manifest.package_name,
:name => apk.label,
:version => apk.manifest.version_code,
:short_version => apk.manifest.version_name
}
if more
info[:icons] = apk.icon.map do |name, data|
tfile = Tempfile.new ["icon-#{ SecureRandom.hex }", '.png']
tfile.write data
@tmpfiles.push tfile
{
:name => File.basename(name),
:path => tfile.path
}
end
end
info
end
def _ipa_info(path, more = false)
path = _path path
ipa = Lagunitas::IPA.new path
_puts '> 正在解析 ipa 文件...'
app = ipa.app
info = {
:type => 'ios',
:identifier => app.identifier,
:name => app.name,
:display_name => app.display_name,
:version => app.version,
:short_version => app.short_version,
:devices => app.devices,
:release_type => app.release_type,
:distribution_name => app.distribution_name
}
if more
if app.icons
info[:icons] = app.icons.map do |icon|
tfile = Tempfile.new ["icon-#{ SecureRandom.hex }", '.png']
@tmpfiles.push tfile
FileUtils.cp icon[:path], tfile.path
{
:name => File.basename(icon[:path]),
:path => tfile.path
}
end
end
info[:plist] = app.info
app.mobileprovision.delete 'DeveloperCertificates' if app.mobileprovision
info[:mobileprovision] = app.mobileprovision
end
ipa.cleanup
info
end
def _fir_info(identifier, type = 'ios')
_puts "> 正在获取 #{ identifier }@FIR.im 的应用信息..."
_chk_login
body = { :token => @token, :type => type }
res = RestClient.get "http://fir.im/api/v2/app/info/#{ identifier }?#{ URI.encode_www_form body }"
JSON.parse res.body, :symbolize_names => true
end
def _fir_put(id, body)
_chk_login
body[:token] = @token
_puts '> 正在更新 fir 的应用信息...'
RestClient.put "http://fir.im/api/v2/app/#{ id }?#{ URI.encode_www_form body }", body
_puts '> 更新成功'
end
def _fir_vput(id, body)
_chk_login
body[:token] = @token
_puts '> 正在更新 fir 的应用版本信息...'
RestClient.put "http://fir.im/api/v2/appVersion/#{ id }/complete?#{ URI.encode_www_form body }", body
_puts '> 更新成功'
end
end
end

56
lib/lagunitas.ext.rb Normal file
View File

@@ -0,0 +1,56 @@
require 'cfpropertylist'
require 'lagunitas'
module Lagunitas
class App
def initialize(path, root)
@path = path
@root = root
end
def name
@info['CFBundleName']
end
def mobileprovision
return if !mobileprovision?
@mobileprovision ||= CFPropertyList.native_types CFPropertyList::List.new(:data => `security cms -D -i #{File.join @path, 'embedded.mobileprovision'}`).value
end
def mobileprovision?
return true if @mobileprovision
File.exists? File.join @path, 'embedded.mobileprovision'
end
def devices
mobileprovision['ProvisionedDevices'] if mobileprovision
end
def distribution_name
# mobileprovision['DeveloperCertificates'] if mobileprovision
"#{ mobileprovision['Name'] }: #{ mobileprovision['TeamName'] }" if mobileprovision
end
def metadata
return if !metadata?
@metadata ||= CFPropertyList.native_types(CFPropertyList::List.new(file: File.join(@root, 'iTunesMetadata.plist')).value)
end
def metadata?
return true if @metadata
File.exists? File.join @root, 'iTunesMetadata.plist'
end
def release_type
@release_type ||= begin
if mobileprovision?
if devices
'adhoc'
else
'inhouse'
end
elsif metadata?
'store'
else
'adhoc'
end
end
end
end
class IPA
def root_path
contents
end
end
end

40
lib/lagunitas.patch.rb Normal file
View File

@@ -0,0 +1,40 @@
require 'lagunitas'
module Lagunitas
class App
def initialize(path, root)
@path = path
@root = root
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
# info['CFBundleIcons'] might be nil
nil
end
end
end
class IPA
def app
@app ||= App.new app_path, root_path
end
def contents
@contents ||= begin
tmp_path = "tmp/lagunitas-#{SecureRandom.hex}"
Zip::ZipFile.open @path do |zip_file|
zip_file.each do |f|
f_path = File.join tmp_path, f.name
FileUtils.mkdir_p File.dirname f_path
zip_file.extract f, f_path unless File.exist? f_path
end
end
tmp_path
end
end
end
end