Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Initial Commit

  • Loading branch information...
commit ac442ee80d63ee0721df8def9be61d955110a168 0 parents
@mattt mattt authored
3  Gemfile
@@ -0,0 +1,3 @@
+source :rubygems
+
+gemspec
34 Gemfile.lock
@@ -0,0 +1,34 @@
+PATH
+ remote: .
+ specs:
+ shenzhen (0.0.1)
+ commander (~> 4.1.2)
+ httmultiparty (~> 0.3.8)
+ json (~> 1.7.3)
+
+GEM
+ remote: http://rubygems.org/
+ specs:
+ commander (4.1.2)
+ highline (~> 1.6.11)
+ highline (1.6.15)
+ httmultiparty (0.3.8)
+ httparty (>= 0.7.3)
+ multipart-post
+ httparty (0.9.0)
+ multi_json (~> 1.0)
+ multi_xml
+ json (1.7.5)
+ multi_json (1.3.6)
+ multi_xml (0.5.1)
+ multipart-post (1.1.5)
+ rake (0.9.2.2)
+ rspec (0.6.4)
+
+PLATFORMS
+ ruby
+
+DEPENDENCIES
+ rake (~> 0.9.2)
+ rspec (~> 0.6.1)
+ shenzhen!
19 LICENSE
@@ -0,0 +1,19 @@
+Copyright (c) 2012 Mattt Thompson (http://mattt.me/)
+
+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.
59 README.md
@@ -0,0 +1,59 @@
+# Shenzhen
+**CLI for Building & Distributing iOS Apps (.ipa Files)**
+
+Create `.ipa` files and then distribute them to TestFlight, all from the command line!
+
+Less cumbersome than clicking around in Xcode, and less hassle than rolling your own build script--Shenzhen radically improves the process of getting new builds out to testers and enterprises.
+
+This project is starting with TestFlight, but will move to support other popular distribution methods, such as S3, CloudApp, and /or Dropbox. Suggestions (and pull requests) are very welcome.
+
+> `shenzhen` is named for [深圳](http://en.wikipedia.org/wiki/Shenzhen), the Chinese city famous for its role as the center of manufacturing for a majority of consumer electronics, including iPhones and iPads. Its [sister project](https://github.com/mattt/cupertino)'s namesake, [Cupertino, CA](http://en.wikipedia.org/wiki/Cupertino,_California), is home to Apple, Inc.'s world headquarters.
+
+## Installation
+
+```
+$ gem install shenzhen
+```
+
+## Usage
+
+Shenzhen adds the `ipa` command to your PATH,
+
+```
+$ ipa
+
+ Build and distribute iOS apps (.ipa files)
+
+ Commands:
+ build Create a new .ipa file for your app
+ distribute:testflight Distribute an .ipa file over testflight
+ help Display global or [command] help documentation.
+
+ Aliases:
+ distribute distribute:testflight
+
+ Global Options:
+ -h, --help Display help documentation
+ -v, --version Display version information
+ -t, --trace Display backtrace when an error occurs
+```
+
+### Building & Distribution
+
+```
+$ cd /path/to/iOS Project/
+$ ipa build
+$ ipa distribute
+```
+
+## Contact
+
+Mattt Thompson
+
+- http://github.com/mattt
+- http://twitter.com/mattt
+- m@mattt.me
+
+## License
+
+Shenzhen is available under the MIT license. See the LICENSE file for more info.
10 Rakefile
@@ -0,0 +1,10 @@
+require "bundler"
+Bundler.setup
+
+gemspec = eval(File.read("shenzhen.gemspec"))
+
+task :build => "#{gemspec.full_name}.gem"
+
+file "#{gemspec.full_name}.gem" => gemspec.files + ["shenzhen.gemspec"] do
+ system "gem build shenzhen.gemspec"
+end
20 bin/ipa
@@ -0,0 +1,20 @@
+#!/usr/bin/env ruby
+
+require 'commander/import'
+
+$:.push File.expand_path("../../lib", __FILE__)
+require 'shenzhen'
+
+HighLine.track_eof = false # Fix for built-in Ruby
+
+program :version, Shenzhen::VERSION
+program :description, 'Build and distribute iOS apps (.ipa files)'
+
+program :help, 'Author', 'Mattt Thompson <m@mattt.me>'
+program :help, 'Website', 'http://mattt.me'
+program :help_formatter, :compact
+
+# global_option '--verbose'
+default_command :help
+
+require 'shenzhen/commands'
6 lib/shenzhen.rb
@@ -0,0 +1,6 @@
+module Shenzhen
+ VERSION = '0.0.1'
+end
+
+require 'shenzhen/agvtool'
+require 'shenzhen/xcodebuild'
17 lib/shenzhen/agvtool.rb
@@ -0,0 +1,17 @@
+module Shenzhen::Agvtool
+ class << self
+ def what_version
+ output = `agvtool what-version -terse`
+ output.length > 0 ? output : nil
+ end
+
+ alias :vers :what_version
+
+ def what_marketing_version
+ output = `agvtool what-marketing-version -terse`
+ output.scan(/\=(.+)$/).flatten.first
+ end
+
+ alias :mvers :what_marketing_version
+ end
+end
7 lib/shenzhen/commands.rb
@@ -0,0 +1,7 @@
+$:.push File.expand_path('../', __FILE__)
+
+require 'plugins/testflight'
+
+require 'commands/build'
+require 'commands/distribute'
+
101 lib/shenzhen/commands/build.rb
@@ -0,0 +1,101 @@
+command :build do |c|
+ c.syntax = 'ipa build [options] [output]'
+ c.summary = 'Create a new .ipa file for your app'
+ c.description = ''
+
+ c.example 'description', 'ipa release 478 --tag 3.7.2 --scheme MyApp'
+ c.option '-w', '--workspace WORKSPACE', 'Workspace (.xcworkspace) file to use to build app (automatically detected in current directory)'
+ c.option '-p', '--project PROJECT', 'Project (.xcodeproj) file to use to build app (automatically detected in current directory, overridden by --workspace option, if passed)'
+ c.option '-c', '--configuration CONFIGURATION', 'Configuration used to build'
+ c.option '-s', '--scheme SCHEME', 'Scheme used to build app'
+ c.option '-q', '--quiet', 'Silence warning and success messages'
+
+ c.action do |args, options|
+ validate_xcode_version!
+
+ @xcodebuild_info = Shenzhen::XcodeBuild.info
+
+ @workspace = options.workspace
+ @project = options.project
+ @scheme = options.scheme
+ @configuration = options.configuration
+
+ determine_workspace_or_project! unless @workspace || @project
+
+ determine_configuration! unless @configuration
+ say_error "Configuration #{@configuration} not found" and abort unless @xcodebuild_info.build_configurations.include?(@configuration)
+
+ determine_scheme! unless @scheme
+ say_error "Scheme #{@scheme} not found" and abort unless @xcodebuild_info.schemes.include?(@scheme)
+
+ say_warning "Building \"#{@workspace || @project}\" with Scheme \"#{@scheme}\" and Configuration \"#{@configuration}\"\n" unless options.quiet
+
+ log "xcodebuild", (@workspace || @project)
+
+ flags = []
+ flags << "-sdk iphoneos"
+ flags << "-workspace '#{@workspace}'" if @workspace
+ flags << "-project '#{@project}'" if @project
+ flags << "-scheme '#{@scheme}'" if @scheme
+ flags << "-configuration Release"
+
+ ENV['CC'] = nil # Fix for RVM
+ abort unless system "xcodebuild #{flags.join(' ')} clean build 1> /dev/null"
+
+ log "xcrun", "PackageApplication"
+
+ @xcodebuild_settings = Shenzhen::XcodeBuild.settings(flags)
+
+ @app_path = File.join(@xcodebuild_settings['BUILT_PRODUCTS_DIR'], @xcodebuild_settings['PRODUCT_NAME']) + ".app"
+ @dsym_path = @app_path + ".dSYM"
+ @ipa_path = File.join(Dir.pwd, @xcodebuild_settings['PRODUCT_NAME']) + ".ipa"
+ abort unless system %{xcrun -sdk iphoneos PackageApplication -v "#{@app_path}" -o "#{@ipa_path}" --embed "#{@dsym_path}" 1> /dev/null}
+
+ say_ok "#{File.basename(@ipa_path)} successfully built" unless options.quiet
+ end
+
+ private
+
+ def validate_xcode_version!
+ version = Shenzhen::XcodeBuild.version
+ say_error "Shenzhen requires Xcode 4 (found #{version}). Please install or switch to the latest Xcode." and abort if version < "4.0.0"
+ end
+
+ def determine_workspace_or_project!
+ workspaces, projects = Dir["*.xcworkspace"], Dir["*.xcodeproj"]
+
+ if workspaces.empty?
+ if projects.empty?
+ say_error "No Xcode projects or workspaces found in current directory" and abort
+ else
+ if projects.length == 1
+ @project = projects.first
+ else
+ @project = choose "Select a project:", *projects
+ end
+ end
+ else
+ if workspaces.length == 1
+ @workspace = workspaces.first
+ else
+ @workspace = choose "Select a workspace:", *workspaces
+ end
+ end
+ end
+
+ def determine_scheme!
+ if @xcodebuild_info.schemes.length == 1
+ @scheme = @xcodebuild_info.schemes.first
+ else
+ @scheme = choose "Select a scheme:", *@xcodebuild_info.schemes
+ end
+ end
+
+ def determine_configuration!
+ if @xcodebuild_info.build_configurations.length == 1
+ @configuration = @xcodebuild_info.build_configurations.first
+ else
+ @configuration = choose "Select a configuration:", *@xcodebuild_info.build_configurations
+ end
+ end
+end
1  lib/shenzhen/commands/distribute.rb
@@ -0,0 +1 @@
+alias_command :distribute, :'distribute:testflight'
104 lib/shenzhen/plugins/testflight.rb
@@ -0,0 +1,104 @@
+require 'openssl'
+require 'faraday'
+require 'faraday_middleware'
+
+module Shenzhen::Plugins
+ module TestFlight
+ class Client
+ HOSTNAME = 'testflightapp.com'
+
+ def initialize(api_token, team_token)
+ @api_token, @team_token = api_token, team_token
+ @connection = Faraday.new(:url => "http://#{HOSTNAME}") do |builder|
+ builder.request :multipart
+ builder.request :json
+ builder.response :json, :content_type => /\bjson$/
+ builder.use FaradayMiddleware::FollowRedirects
+ builder.adapter :net_http
+ end
+ end
+
+ def upload_build(ipa, options)
+ options.update({
+ :api_token => @api_token,
+ :team_token => @team_token,
+ :file => Faraday::UploadIO.new(ipa, 'application/octet-stream')
+ })
+
+ @connection.post("/api/builds.json", options).on_complete do |env|
+ yield env[:status], env[:body] if block_given?
+ end
+ end
+ end
+ end
+end
+
+command :'distribute:testflight' do |c|
+ c.syntax = "ipa distribute:testflight [options]"
+ c.summary = "Distribute an .ipa file over testflight"
+ c.description = ""
+ c.option '-f', '--file FILE', ".ipa file for the build"
+ c.option '-t', '--api_token TOKEN', "API Token. Available at https://testflightapp.com/account/#api-token"
+ c.option '-T', '--team_token TOKEN', "Team Token. Available at https://testflightapp.com/dashboard/team/edit/"
+ c.option '-m', '--notes NOTES', "Release notes for the build"
+ c.option '-l', '--lists LISTS', "Comma separated distribution list names which will receive access to the build"
+ c.option '--notify', "Notify permitted teammates to install the build"
+ c.option '--replace', "Replace binary for an existing build if one is found with the same name/bundle version"
+ c.option '-q', '--quiet', "Silence warning and success messages"
+
+ c.action do |args, options|
+ determine_file! unless @file = options.file
+ say_error "Missing or unspecified .ipa file" and abort unless @file and File.exist?(@file)
+
+ determine_api_token! unless @api_token = options.api_token
+ say_error "Missing API Token" and abort unless @api_token
+
+ determine_team_token! unless @team_token = options.team_token
+
+ determine_notes! unless @notes = options.notes
+ say_error "Missing release notes" and abort unless @notes
+
+ parameters = {}
+ parameters[:file] = @file
+ parameters[:notes] = @notes
+ parameters[:notify] = "true" if options.notify
+ parameters[:replace] = "true" if options.replace
+ parameters[:distribution_lists] = options.lists if options.lists
+
+ client = Shenzhen::Plugins::TestFlight::Client.new(@api_token, @team_token)
+ response = client.upload_build(@file, parameters)
+ case response.status
+ when 200...300
+ say_ok "Build successfully uploaded to TestFlight"
+ else
+ say_error "Error uploading to TestFlight: #{response.body}"
+ end
+ end
+
+ private
+
+ def determine_api_token!
+ @api_token ||= ask "API Token:"
+ end
+
+ def determine_team_token!
+ @team_token ||= ask "Team Token:"
+ end
+
+ def determine_file!
+ files = Dir['*.ipa']
+ @file ||= case files.length
+ when 0 then nil
+ when 1 then files.first
+ else
+ @file = choose "Select an .ipa File:", *files
+ end
+ end
+
+ def determine_notes!
+ placeholder = %{What's new in this release: }
+
+ @notes = ask_editor placeholder
+ @notes = nil if @notes == placeholder
+ end
+end
63 lib/shenzhen/xcodebuild.rb
@@ -0,0 +1,63 @@
+require 'ostruct'
+
+module Shenzhen::XcodeBuild
+ class Info < OpenStruct; end
+
+ class Error < StandardError; end
+ class NilOutputError < Error; end
+
+ class << self
+ def info
+ output = `xcodebuild -list 2> /dev/null`
+ raise Error.new $1 if /^xcodebuild\: error\: (.+)$/ === output
+ raise NilOutputError unless /\S/ === output
+
+ lines = output.split(/\n/)
+ hash = {}
+ group = nil
+
+ hash[:project] = lines.shift.match(/\"(.+)\"\:/)[1]
+
+ lines.each do |line|
+ if /\:$/ === line
+ group = line.strip[0...-1].downcase.gsub(/\s+/, '_')
+ hash[group] = []
+ next
+ end
+
+ unless group.nil? or /\.$/ === line
+ hash[group] << line.strip
+ end
+ end
+
+ hash.each do |group, values|
+ next unless Array === values
+ values.delete("") and values.uniq!
+ end
+
+ Info.new(hash)
+ end
+
+ def settings(flags = [])
+ output = `xcodebuild #{flags.join(' ')} -showBuildSettings 2> /dev/null`
+ raise Error.new $1 if /^xcodebuild\: error\: (.+)$/ === output
+ raise NilOutputError unless /\S/ === output
+
+ lines = output.split(/\n/)
+ lines.shift
+
+ hash = {}
+ lines.each do |line|
+ key, value = line.split(/\=/).collect(&:strip)
+ hash[key] = value
+ end
+
+ hash
+ end
+
+ def version
+ output = `xcodebuild -version`
+ output.scan(/([\d\.?]+)/).flatten.first rescue nil
+ end
+ end
+end
27 shenzhen.gemspec
@@ -0,0 +1,27 @@
+# -*- encoding: utf-8 -*-
+$:.push File.expand_path("../lib", __FILE__)
+require "shenzhen"
+
+Gem::Specification.new do |s|
+ s.name = "shenzhen"
+ s.authors = ["Mattt Thompson"]
+ s.email = "m@mattt.me"
+ s.homepage = "http://github.com/mattt/shenzhen"
+ s.version = Shenzhen::VERSION
+ s.platform = Gem::Platform::RUBY
+ s.summary = "Shenzhen"
+ s.description = "CLI for Building & Distributing iOS Apps (.ipa Files)"
+
+ s.add_development_dependency "rspec", "~> 0.6.1"
+ s.add_development_dependency "rake", "~> 0.9.2"
+
+ s.add_dependency "commander", "~> 4.1.2"
+ s.add_dependency "json", "~> 1.7.3"
+ s.add_dependency "faraday", "~> 0.8.0"
+ s.add_dependency "faraday_middleware", "~> 0.8.7"
+
+ s.files = Dir["./**/*"].reject { |file| file =~ /\.\/(bin|log|pkg|script|spec|test|vendor)/ }
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
+ s.require_paths = ["lib"]
+end
Please sign in to comment.
Something went wrong with that request. Please try again.