├── .gitignore ├── .rubocop.yml ├── .swift-version ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── bin └── grenouille ├── circle.yml ├── grenouille.gemspec ├── lib ├── grenouille.rb └── grenouille │ ├── swift_update.rb │ ├── version.rb │ └── xcode.rb ├── perfume.gif └── spec ├── fixtures ├── Blueprint.swift ├── ChoreTask.swift ├── Mac-1.1.swift └── Stargate.swift ├── spec_helper.rb ├── swift_update_spec.rb └── xcode_spec.rb /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | *.bundle 11 | *.so 12 | *.o 13 | *.a 14 | mkmf.log 15 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | Lint/Void: 2 | Enabled: false 3 | 4 | Metrics/AbcSize: 5 | Max: 30 6 | 7 | Metrics/LineLength: 8 | Max: 142 9 | 10 | Metrics/MethodLength: 11 | Max: 18 12 | 13 | Style/Documentation: 14 | Enabled: false 15 | -------------------------------------------------------------------------------- /.swift-version: -------------------------------------------------------------------------------- 1 | 1.2 2 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | 5 | gem 'xcinvoke', git: 'https://github.com/segiddins/xcinvoke' 6 | 7 | group :development do 8 | gem 'bacon' 9 | gem 'mocha-on-bacon' 10 | gem 'mocha', '~> 0.11.4' 11 | gem 'prettybacon', git: 'git@github.com:irrationalfab/PrettyBacon.git', branch: 'master' 12 | gem 'coveralls', require: false 13 | end 14 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Boris Bügling 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Grenouille 2 | 3 | [![No Maintenance Intended](http://unmaintained.tech/badge.svg)](http://unmaintained.tech/) 4 | [![Build Status](https://img.shields.io/circleci/project/neonichu/grenouille.svg?style=flat)](https://circleci.com/gh/neonichu/grenouille) 5 | [![Coverage Status](https://coveralls.io/repos/neonichu/grenouille/badge.svg)](https://coveralls.io/r/neonichu/grenouille) 6 | [![Gem Version](http://img.shields.io/gem/v/grenouille.svg?style=flat)](http://badge.fury.io/rb/grenouille) 7 | [![Code Climate](http://img.shields.io/codeclimate/github/neonichu/grenouille.svg?style=flat)](https://codeclimate.com/github/neonichu/grenouille) 8 | 9 | ![](perfume.gif) 10 | 11 | Grenouille uses Apple's `swift-update` tool to determine the version of Swift 12 | used in a set of files automatically. 13 | 14 | ## Usage 15 | 16 | You can use it on the commandline: 17 | 18 | ```bash 19 | $ grenouille Stargate.swift 20 | 1.2 21 | ``` 22 | 23 | or programmatically: 24 | 25 | ```ruby 26 | su = Grenouille::SwiftUpdate.new 27 | su.determine_version('Stargate.swift') 28 | # 1.2 29 | ``` 30 | 31 | You can also get more detailled output: 32 | 33 | ```ruby 34 | su.update_to_latest_swift('spec/fixtures/Blueprint.swift') 35 | # {:report=>[{"file"=>"spec/fixtures/Blueprint.swift", "offset"=>740, "remove"=>2, "text"=>"as!"}, {"file"=>"spec/fixtures/Blueprint.swift", "offset"=>6758, "remove"=>2, "text"=>"as!"}, {"file"=>"spec/fixtures/Blueprint.swift", "offset"=>10613, "remove"=>2, "text"=>"as!"}], :output=>""} 36 | ``` 37 | 38 | The value for the `:report` key represents the result of `swift-update`, a 39 | list of changes to perform to transform the current code to the version of 40 | Swift supported by the used Xcode. The value for the `:output` key is the 41 | aggregate of stdout and stderr of `swift-update`, e.g. compilation errors. 42 | 43 | ## Installation 44 | 45 | Add this line to your application's Gemfile: 46 | 47 | ```ruby 48 | gem 'grenouille' 49 | ``` 50 | 51 | And then execute: 52 | 53 | $ bundle 54 | 55 | Or install it yourself as: 56 | 57 | $ gem install grenouille 58 | 59 | Note: The `swift-update` tool is a part of Xcode since 6.3, so it 60 | needs at least that version to function. 61 | 62 | ## Contributing 63 | 64 | 1. Fork it ( https://github.com/neonichu/grenouille/fork ) 65 | 2. Create your feature branch (`git checkout -b my-new-feature`) 66 | 3. Commit your changes (`git commit -am 'Add some feature'`) 67 | 4. Push to the branch (`git push origin my-new-feature`) 68 | 5. Create a new Pull Request 69 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | 3 | def specs(dir) 4 | FileList["spec/#{dir}/*_spec.rb"].shuffle.join(' ') 5 | end 6 | 7 | desc 'Runs all the specs' 8 | task :spec do 9 | sh "bundle exec bacon #{specs('**')}" 10 | end 11 | 12 | task default: :spec 13 | -------------------------------------------------------------------------------- /bin/grenouille: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | if $PROGRAM_NAME == __FILE__ 4 | ENV['BUNDLE_GEMFILE'] = File.expand_path('../../Gemfile', __FILE__) 5 | require 'rubygems' 6 | require 'bundler/setup' 7 | $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) 8 | end 9 | 10 | require 'grenouille' 11 | 12 | su = Grenouille::SwiftUpdate.new 13 | 14 | if su.current_version < Gem::Version.new('6.3') 15 | puts 'Please install Xcode 6.3 or newer.' 16 | exit(1) 17 | end 18 | 19 | begin 20 | puts su.determine_version(ARGV) 21 | rescue StandardError => msg 22 | puts "Error: #{msg}" 23 | exit(1) 24 | end 25 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | environment: 3 | LANG: en_US.UTF-8 4 | XCODE_PROJECT: yolo.xcodeproj 5 | XCODE_SCHEME: shrugs 6 | xcode: 7 | version: "6.3.1" 8 | test: 9 | override: 10 | - rake spec 11 | -------------------------------------------------------------------------------- /grenouille.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'grenouille/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = 'grenouille' 8 | spec.version = Grenouille::VERSION 9 | spec.authors = ['Boris Bügling'] 10 | spec.email = ['boris@buegling.com'] 11 | spec.summary = 'Automatically detect Swift versions from source code.' 12 | spec.homepage = 'https://github.com/neonichu/grenouille' 13 | spec.license = 'MIT' 14 | 15 | spec.files = `git ls-files -z`.split("\x0") 16 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 17 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 18 | spec.require_paths = ['lib'] 19 | 20 | spec.add_runtime_dependency 'xcinvoke' 21 | 22 | spec.add_development_dependency 'bundler', '~> 1.7' 23 | spec.add_development_dependency 'rake', '~> 10.0' 24 | end 25 | -------------------------------------------------------------------------------- /lib/grenouille.rb: -------------------------------------------------------------------------------- 1 | require 'grenouille/swift_update' 2 | require 'grenouille/version' 3 | require 'grenouille/xcode' 4 | -------------------------------------------------------------------------------- /lib/grenouille/swift_update.rb: -------------------------------------------------------------------------------- 1 | require 'grenouille/xcode' 2 | require 'pathname' 3 | require 'tempfile' 4 | require 'yaml' 5 | 6 | # check for xcrun: error: unable to find utility "swift-update", not a developer tool or in PATH 7 | 8 | module Grenouille 9 | class SwiftUpdate < Xcode 10 | def determine_version(file_glob) 11 | required_changes = update_to_latest_swift(file_glob)[:report] 12 | required_changes.count == 0 ? swift_version : previous_swift_version 13 | end 14 | 15 | def update_to_latest_swift(file_glob) 16 | fail 'Non-Swift files in input' if Pathname.glob(file_glob).reject { |f| f.extname == '.swift' }.count > 0 17 | files = Dir.glob(file_glob) 18 | fail 'No files in input' if files.empty? 19 | 20 | platform = guess_platform(file_glob) 21 | sdk_path = xcrun(%W(-sdk #{platform} -show-sdk-path)).chomp 22 | sdk_version = xcrun(%W(-sdk #{platform} -show-sdk-version)).chomp 23 | target = platform == :iphoneos ? %W(-target arm64-apple-ios#{sdk_version}) : [] 24 | 25 | output = xcrun(%W(-sdk #{platform} swift-update)) 26 | fail 'Unable to find "swift-update", install Xcode 6.3 or later' if output =~ /unable to find utility/ 27 | 28 | Tempfile.open('swift-update.yaml') do |temp_file| 29 | temp_file.close 30 | args = %W(-sdk #{platform} swift-update -sdk #{sdk_path}) 31 | args += files + target + %W(-o #{temp_file.path}) 32 | output = xcrun(args, err: :merge) 33 | report = YAML.load(File.read(temp_file)) 34 | { report: report, output: output } 35 | end 36 | end 37 | 38 | private 39 | 40 | def guess_platform(file_glob) 41 | Dir.glob(file_glob).each do |file| 42 | content = File.read(file) 43 | return :macosx if content =~ /^import AppKit$/ 44 | return :macosx if content =~ /NSTask/ 45 | end 46 | 47 | :iphoneos 48 | end 49 | 50 | def previous_swift_version 51 | current = swift_version.segments 52 | Gem::Version.new("#{current[0]}.#{current[1] - 1}") 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /lib/grenouille/version.rb: -------------------------------------------------------------------------------- 1 | module Grenouille 2 | VERSION = '0.0.1' 3 | end 4 | -------------------------------------------------------------------------------- /lib/grenouille/xcode.rb: -------------------------------------------------------------------------------- 1 | require 'xcinvoke' 2 | 3 | module Grenouille 4 | class Xcode < XCInvoke::Xcode 5 | def self.new(developer_dir = nil) 6 | if developer_dir 7 | super 8 | else 9 | selected 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /perfume.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neonichu/grenouille/3158621815847d90a8f8979f4f53a023c63e3c3a/perfume.gif -------------------------------------------------------------------------------- /spec/fixtures/Blueprint.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Blueprint.swift 3 | // Representor 4 | // 5 | // Created by Kyle Fuller on 06/01/2015. 6 | // Copyright (c) 2015 Apiary. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | // MARK: Models 12 | 13 | /// A structure representing an API Blueprint AST 14 | public struct Blueprint { 15 | /// Name of the API 16 | public let name:String 17 | 18 | /// Top-level description of the API in Markdown (.raw) or HTML (.html) 19 | public let description:String? 20 | 21 | /// The collection of resource groups 22 | public let resourceGroups:[ResourceGroup] 23 | 24 | public init(name:String, description:String?, resourceGroups:[ResourceGroup]) { 25 | self.name = name 26 | self.description = description 27 | self.resourceGroups = resourceGroups 28 | } 29 | 30 | public init(ast:[String:AnyObject]) { 31 | name = ast["name"] as String 32 | description = ast["description"] as? String 33 | resourceGroups = parseBlueprintResourceGroups(ast) 34 | } 35 | 36 | public init?(named:String, bundle:NSBundle? = nil) { 37 | func loadFile(named:String, bundle:NSBundle) -> [String:AnyObject]? { 38 | if let url = bundle.URLForResource(named, withExtension: nil) { 39 | if let data = NSData(contentsOfURL: url) { 40 | let object: AnyObject? = NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions(0), error: nil) 41 | return object as? [String:AnyObject] 42 | } 43 | } 44 | 45 | return nil 46 | } 47 | 48 | let ast = loadFile(named, bundle ?? NSBundle.mainBundle()) 49 | if let ast = ast { 50 | self.init(ast: ast) 51 | } else { 52 | return nil 53 | } 54 | } 55 | } 56 | 57 | /// Logical group of resources. 58 | public struct ResourceGroup { 59 | /// Name of the Resource Group 60 | public let name:String 61 | 62 | /// Description of the Resource Group (.raw or .html) 63 | public let description:String? 64 | 65 | /// Array of the respective resources belonging to the Resource Group 66 | public let resources:[Resource] 67 | 68 | public init(name:String, description:String?, resources:[Resource]) { 69 | self.name = name 70 | self.description = description 71 | self.resources = resources 72 | } 73 | } 74 | 75 | /// Description of one resource, or a cluster of resources defined by its URI template 76 | public struct Resource { 77 | /// Name of the Resource 78 | public let name:String 79 | 80 | /// Description of the Resource (.raw or .html) 81 | public let description:String? 82 | 83 | /// URI Template as defined in RFC6570 84 | // TODO, make this a URITemplate object 85 | public let uriTemplate:String 86 | 87 | /// Array of URI parameters 88 | public let parameters:[Parameter] 89 | 90 | /// Array of actions available on the resource each defining at least one complete HTTP transaction 91 | public let actions:[Action] 92 | 93 | public init(name:String, description:String?, uriTemplate:String, parameters:[Parameter], actions:[Action]) { 94 | self.name = name 95 | self.description = description 96 | self.uriTemplate = uriTemplate 97 | self.actions = actions 98 | self.parameters = parameters 99 | } 100 | } 101 | 102 | /// Description of one URI template parameter 103 | public struct Parameter { 104 | /// Name of the parameter 105 | public let name:String 106 | 107 | /// Description of the Parameter (.raw or .html) 108 | public let description:String? 109 | 110 | /// An arbitrary type of the parameter (a string) 111 | public let type:String? 112 | 113 | /// Boolean flag denoting whether the parameter is required (true) or not (false) 114 | public let required:Bool 115 | 116 | /// A default value of the parameter (a value assumed when the parameter is not specified) 117 | public let defaultValue:String? 118 | 119 | /// An example value of the parameter 120 | public let example:String? 121 | 122 | /// An array enumerating possible parameter values 123 | public let values:[String]? 124 | 125 | public init(name:String, description:String?, type:String?, required:Bool, defaultValue:String?, example:String?, values:[String]?) { 126 | self.name = name 127 | self.description = description 128 | self.type = type 129 | self.required = required 130 | self.defaultValue = defaultValue 131 | self.example = example 132 | self.values = values 133 | } 134 | } 135 | 136 | // An HTTP transaction (a request-response transaction). Actions are specified by an HTTP request method within a resource 137 | public struct Action { 138 | /// Name of the Action 139 | public let name:String 140 | 141 | /// Description of the Action (.raw or .html) 142 | public let description:String? 143 | 144 | /// HTTP request method defining the action 145 | public let method:String 146 | 147 | /// Array of URI parameters 148 | public let parameters:[Parameter] 149 | 150 | /// URI Template for the action, if it differs from the resource's URI 151 | public let uriTemplate:String? 152 | 153 | /// Link relation identifier of the action 154 | public let relation:String? 155 | 156 | /// HTTP transaction examples for the relevant HTTP request method 157 | public let examples:[TransactionExample] 158 | 159 | public init(name:String, description:String?, method:String, parameters:[Parameter], uriTemplate:String? = nil, relation:String? = nil, examples:[TransactionExample]? = nil) { 160 | self.name = name 161 | self.description = description 162 | self.method = method 163 | self.parameters = parameters 164 | self.uriTemplate = uriTemplate 165 | self.relation = relation 166 | self.examples = examples ?? [] 167 | } 168 | } 169 | 170 | /// An HTTP transaction example with expected HTTP message request and response payload 171 | public struct TransactionExample { 172 | /// Name of the Transaction Example 173 | public let name:String 174 | 175 | /// Description of the Transaction Example (.raw or .html) 176 | public let description:String? 177 | 178 | /// Example transaction request payloads 179 | public let requests:[Payload] 180 | 181 | /// Example transaction response payloads 182 | public let responses:[Payload] 183 | 184 | public init(name:String, description:String? = nil, requests:[Payload]? = nil, responses:[Payload]? = nil) { 185 | self.name = name 186 | self.description = description 187 | self.requests = requests ?? [] 188 | self.responses = responses ?? [] 189 | } 190 | } 191 | 192 | 193 | /// An API Blueprint payload. 194 | public struct Payload { 195 | public typealias Header = (name:String, value:String) 196 | 197 | /// Name of the payload 198 | public let name:String 199 | 200 | /// Description of the Payload (.raw or .html) 201 | public let description:String? 202 | 203 | /// HTTP headers that are expected to be transferred with HTTP message represented by this payload 204 | public let headers:[Header] 205 | 206 | /// An entity body to be transferred with HTTP message represented by this payload 207 | public let body:NSData? 208 | 209 | public init(name:String, description:String? = nil, headers:[Header]? = nil, body:NSData? = nil) { 210 | self.name = name 211 | self.description = description 212 | self.headers = headers ?? [] 213 | self.body = body 214 | } 215 | } 216 | 217 | 218 | // MARK: AST Parsing 219 | 220 | func compactMap(source: C, transform: (C.Generator.Element) -> T?) -> [T] { 221 | var collection = [T]() 222 | 223 | for element in source { 224 | if let item = transform(element) { 225 | collection.append(item) 226 | } 227 | } 228 | 229 | return collection 230 | } 231 | 232 | func parseParameter(source:[[String:AnyObject]]?) -> [Parameter] { 233 | if let source = source { 234 | return source.map { item in 235 | let name = item["name"] as String 236 | let description = item["description"] as? String 237 | let type = item["type"] as? String 238 | let required = item["required"] as? Bool 239 | let defaultValue = item["default"] as? String 240 | let example = item["example"] as? String 241 | let values = item["values"] as? [String] 242 | return Parameter(name: name, description: description, type: type, required: required ?? true, defaultValue: defaultValue, example: example, values: values) 243 | } 244 | } 245 | 246 | return [] 247 | } 248 | 249 | func parseActions(source:[[String:AnyObject]]?) -> [Action] { 250 | if let source = source { 251 | return compactMap(source) { item in 252 | let name = item["name"] as? String 253 | let description = item["description"] as? String 254 | let method = item["method"] as? String 255 | let parameters = parseParameter(item["parameters"] as? [[String:AnyObject]]) 256 | let attributes = item["attributes"] as? [String:String] 257 | let uriTemplate = attributes?["uriTemplate"] 258 | let relation = attributes?["relation"] 259 | let examples = parseExamples(item["examples"] as? [[String:AnyObject]]) 260 | 261 | if let name = name { 262 | if let method = method { 263 | return Action(name: name, description: description, method: method, parameters: parameters, uriTemplate:uriTemplate, relation:relation, examples:examples) 264 | } 265 | } 266 | 267 | return nil 268 | } 269 | } 270 | 271 | return [] 272 | } 273 | 274 | func parseExamples(source:[[String:AnyObject]]?) -> [TransactionExample] { 275 | if let source = source { 276 | return compactMap(source) { item in 277 | let name = item["name"] as? String 278 | let description = item["description"] as? String 279 | let requests = parsePayloads(item["requests"] as? [[String:AnyObject]]) 280 | let responses = parsePayloads(item["responses"] as? [[String:AnyObject]]) 281 | 282 | if let name = name { 283 | return TransactionExample(name: name, description: description, requests: requests, responses: responses) 284 | } 285 | 286 | return nil 287 | } 288 | } 289 | 290 | return [] 291 | } 292 | 293 | func parsePayloads(source:[[String:AnyObject]]?) -> [Payload] { 294 | if let source = source { 295 | return compactMap(source) { item in 296 | let name = item["name"] as? String 297 | let description = item["description"] as? String 298 | let headers = parseHeaders(item["headers"] as? [[String:String]]) 299 | let bodyString = item["body"] as? String 300 | let body = bodyString?.dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: true) 301 | 302 | if let name = name { 303 | return Payload(name: name, description: description, headers: headers, body: body) 304 | } 305 | 306 | return nil 307 | } 308 | } 309 | 310 | return [] 311 | } 312 | 313 | func parseHeaders(source:[[String:String]]?) -> [Payload.Header] { 314 | if let source = source { 315 | return compactMap(source) { item in 316 | if let name = item["name"] { 317 | if let value = item["value"] { 318 | return (name, value) 319 | } 320 | } 321 | 322 | return nil 323 | } 324 | } 325 | 326 | return [] 327 | } 328 | 329 | func parseResources(source:[[String:AnyObject]]?) -> [Resource] { 330 | if let source = source { 331 | return compactMap(source) { item in 332 | let name = item["name"] as? String 333 | let description = item["description"] as? String 334 | let uriTemplate = item["uriTemplate"] as? String 335 | let actions = parseActions(item["actions"] as? [[String:AnyObject]]) 336 | let parameters = parseParameter(item["parameters"] as? [[String:AnyObject]]) 337 | 338 | if let name = name { 339 | if let uriTemplate = uriTemplate { 340 | return Resource(name: name, description: description, uriTemplate: uriTemplate, parameters: parameters, actions: actions) 341 | } 342 | } 343 | 344 | return nil 345 | } 346 | } 347 | 348 | return [] 349 | } 350 | 351 | private func parseBlueprintResourceGroups(blueprint:Dictionary) -> [ResourceGroup] { 352 | if let resourceGroups = blueprint["resourceGroups"] as? [[String:AnyObject]] { 353 | return compactMap(resourceGroups) { dictionary in 354 | if let name = dictionary["name"] as String? { 355 | let resources = parseResources(dictionary["resources"] as? [[String:AnyObject]]) 356 | let description = dictionary["description"] as String? 357 | return ResourceGroup(name: name, description: description, resources: resources) 358 | } 359 | 360 | return nil 361 | } 362 | } 363 | 364 | return [] 365 | } 366 | -------------------------------------------------------------------------------- /spec/fixtures/ChoreTask.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// The result of a task execution 4 | public typealias ChoreResult = (result: Int32, stdout: String, stderr: String) 5 | 6 | private func string_trim(string: NSString!) -> String { 7 | return string.stringByTrimmingCharactersInSet(.whitespaceAndNewlineCharacterSet()) ?? "" 8 | } 9 | 10 | private func chore_task(command: String, _ arguments: [String] = [String](), stdin: String = "") -> ChoreResult { 11 | let task = NSTask() 12 | 13 | task.launchPath = command 14 | task.arguments = arguments 15 | 16 | if !(task.launchPath as NSString).absolutePath { 17 | task.launchPath = (chore_task("/usr/bin/which", [task.launchPath])).stdout 18 | } 19 | 20 | var isDirectory: ObjCBool = false 21 | 22 | if !NSFileManager.defaultManager().fileExistsAtPath(task.launchPath, isDirectory: &isDirectory) { 23 | return (255, "", String(format: "%@: launch path not accessible", task.launchPath)) 24 | } 25 | 26 | if (isDirectory) { 27 | return (255, "", String(format: "%@: launch path is a directory", task.launchPath)) 28 | } 29 | 30 | if !NSFileManager.defaultManager().isExecutableFileAtPath(task.launchPath) { 31 | return (255, "", String(format: "%@: launch path not executable", task.launchPath)) 32 | } 33 | 34 | if count(stdin) > 0 { 35 | let stdinPipe = NSPipe() 36 | task.standardInput = stdinPipe 37 | let stdinHandle = stdinPipe.fileHandleForWriting 38 | 39 | if let data = stdin.dataUsingEncoding(NSUTF8StringEncoding) { 40 | stdinHandle.writeData(data) 41 | stdinHandle.closeFile() 42 | } 43 | } 44 | 45 | let stderrPipe = NSPipe() 46 | task.standardError = stderrPipe 47 | let stderrHandle = stderrPipe.fileHandleForReading 48 | 49 | let stdoutPipe = NSPipe() 50 | task.standardOutput = stdoutPipe 51 | let stdoutHandle = stdoutPipe.fileHandleForReading 52 | 53 | task.launch() 54 | task.waitUntilExit() 55 | 56 | let stderr = string_trim(NSString(data: stderrHandle.readDataToEndOfFile(), encoding: NSUTF8StringEncoding)) ?? "" 57 | let stdout = string_trim(NSString(data: stdoutHandle.readDataToEndOfFile(), encoding: NSUTF8StringEncoding)) ?? "" 58 | 59 | return (task.terminationStatus, stdout, stderr) 60 | } 61 | 62 | prefix operator > {} 63 | 64 | /** 65 | Executes a command. 66 | 67 | :param: command The command to execute. 68 | :returns: A tuple containing the exit code, stdout and stderr output. 69 | */ 70 | public prefix func > (command: String) -> ChoreResult { 71 | return chore_task(command) 72 | } 73 | 74 | /** 75 | Executes a command with arguments. 76 | 77 | :param: command The command to execute and its arguments. 78 | :returns: A tuple containing the exit code, stdout and stderr output. 79 | */ 80 | public prefix func > (command: [String]) -> ChoreResult { 81 | switch command.count { 82 | case 0: 83 | return (0, "", "") 84 | case 1: 85 | return chore_task(command[0]) 86 | default: 87 | break 88 | } 89 | 90 | return chore_task(command[0], Array(command[1.. ChoreResult { 103 | return left|[right] 104 | } 105 | 106 | /** 107 | Executes a command with standard input from another command. 108 | 109 | :param: left The result of a previous command. 110 | :param: right The command to execute and its arguments. 111 | :returns: A tuple containing the exit code, stdout and stderr output. 112 | */ 113 | public func | (left: ChoreResult, right: [String]) -> ChoreResult { 114 | if left.result != 0 { 115 | return left 116 | } 117 | 118 | let arguments = right.count >= 2 ? Array(right[1.. String)) -> ChoreResult { 130 | if left.result != 0 { 131 | return left 132 | } 133 | 134 | return (0, right(left.stdout), "") 135 | } 136 | 137 | /** 138 | Executes a command with input from a closure. 139 | 140 | :param: left The closure to execute. 141 | :param: right The command to execute. 142 | :returns: A tuple containing the exit code, stdout and stderr output. 143 | */ 144 | public func | (left: (() -> String), right: String) -> ChoreResult { 145 | return (0, left(), "")|right 146 | } 147 | 148 | /** 149 | Executes a command with input from a closure. 150 | 151 | :param: left The closure to execute. 152 | :param: right The command to execute and its arguments. 153 | :returns: A tuple containing the exit code, stdout and stderr output. 154 | */ 155 | public func | (left: (() -> String), right: [String]) -> ChoreResult { 156 | return (0, left(), "")|right 157 | } 158 | 159 | /** 160 | Executes a command with input from a string. 161 | 162 | :param: left The string to use a stdin. 163 | :param: right The command to execute. 164 | :returns: A tuple containing the exit code, stdout and stderr output. 165 | */ 166 | public func | (left: String, right: String) -> ChoreResult { 167 | return (0, left, "")|right 168 | } 169 | 170 | /** 171 | Executes a command with input from a string. 172 | 173 | :param: left The string to use a stdin. 174 | :param: right The command to execute and its arguments. 175 | :returns: A tuple containing the exit code, stdout and stderr output. 176 | */ 177 | public func | (left: String, right: [String]) -> ChoreResult { 178 | return (0, left, "")|right 179 | } 180 | -------------------------------------------------------------------------------- /spec/fixtures/Mac-1.1.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import AppKit 3 | 4 | private func chore_task() { 5 | let task = NSTask() 6 | let array = [1,2,3] 7 | println(countElements(array)) 8 | } 9 | -------------------------------------------------------------------------------- /spec/fixtures/Stargate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Stargate.swift 3 | // Stargate 4 | // 5 | // Created by Boris Bügling on 28/04/15. 6 | // Copyright (c) 2015 Contentful GmbH. All rights reserved. 7 | // 8 | 9 | import MultipeerConnectivity 10 | import PeerKit 11 | 12 | public typealias DebugHandler = (message: String) -> Void 13 | 14 | public class Base { 15 | private var applicationGroupIdentifier: String = "" 16 | var pingIdentifier = "274EAEF1-A178-47FE-81F4-96E87C242456" 17 | var pingPayload = "ping" 18 | var sanitizedIdentifier: String { 19 | return applicationGroupIdentifier.stringByReplacingOccurrencesOfString(".", withString: "", options: NSStringCompareOptions.allZeros, range: nil).substringToIndex(advance(applicationGroupIdentifier.startIndex, 15)) 20 | } 21 | 22 | public init(applicationGroupIdentifier: String) { 23 | self.applicationGroupIdentifier = applicationGroupIdentifier 24 | } 25 | 26 | public func listenForMessage(#identifier: String, _ listener: ((AnyObject!) -> Void)) { 27 | fatalError("listenForMessage() needs to be overidden in subclasses.") 28 | } 29 | 30 | public func passMessage(message: NSCoding, identifier: String) { 31 | fatalError("passMessageObject() needs to be overidden in subclasses.") 32 | } 33 | 34 | public func sendMultipeerMessage(message: AnyObject, identifier: String) { 35 | let allPeers = PeerKit.session?.connectedPeers as? [MCPeerID] 36 | PeerKit.sendEvent(identifier, object: message, toPeers: allPeers) 37 | } 38 | 39 | public func stopListeningForMessage(#identifier: String) { 40 | fatalError("stopListeningForMessage() needs to be overidden in subclasses.") 41 | } 42 | } 43 | 44 | #if os(iOS) 45 | import MMWormhole 46 | import WatchKit 47 | import UIKit 48 | 49 | /// Stargate endpoint to be used on the phone 50 | public class Abydos : Base { 51 | private var callback: DebugHandler? 52 | var wormhole: MMWormhole! 53 | 54 | public override init(applicationGroupIdentifier: String) { 55 | super.init(applicationGroupIdentifier: applicationGroupIdentifier) 56 | 57 | PeerKit.transceive(sanitizedIdentifier) 58 | sendMultipeerMessage(pingPayload, identifier: pingIdentifier) 59 | 60 | wormhole = MMWormhole(applicationGroupIdentifier: applicationGroupIdentifier, optionalDirectory: "stargate") 61 | wormhole.passMessageObject(pingPayload, identifier: pingIdentifier) 62 | } 63 | 64 | public func debug(callback: DebugHandler) { 65 | PeerKit.onConnect = { (me, you) -> Void in callback(message: "connect: \(me) <=> \(you)") } 66 | self.callback = callback 67 | } 68 | 69 | public func tunnel() { 70 | PeerKit.transceive(sanitizedIdentifier) 71 | PeerKit.onEvent = { (peerID, event, object) -> Void in 72 | if let object = object as? NSCoding { 73 | if let callback = self.callback { 74 | callback(message: "Received message from Mac: \(object) for \(event)") 75 | } 76 | 77 | self.wormhole.passMessageObject(object, identifier: event) 78 | } 79 | } 80 | 81 | //UIApplication.sharedApplication().beginBackgroundTaskWithExpirationHandler() {} 82 | } 83 | 84 | public func tunnelReplies(#identifier: String) { 85 | wormhole.listenForMessageWithIdentifier(identifier) { (message) -> Void in 86 | if let message: AnyObject = message { 87 | if let callback = self.callback { 88 | callback(message: "Received message from watch: \(message) for \(identifier)") 89 | } 90 | 91 | self.sendMultipeerMessage(message, identifier: identifier) 92 | } 93 | } 94 | } 95 | } 96 | 97 | /// Stargate endpoint to be used on the ᴡᴀᴛᴄʜ 98 | public class Atlantis : Base { 99 | var wormhole: MMWormhole! 100 | 101 | public override init(applicationGroupIdentifier: String) { 102 | super.init(applicationGroupIdentifier: applicationGroupIdentifier) 103 | 104 | wormhole = MMWormhole(applicationGroupIdentifier: applicationGroupIdentifier, optionalDirectory: "stargate") 105 | passMessage(pingPayload, identifier: pingIdentifier) 106 | } 107 | 108 | public override func listenForMessage(#identifier: String, _ listener: ((AnyObject!) -> Void)) { 109 | wormhole.listenForMessageWithIdentifier(identifier, listener: listener) 110 | 111 | WKInterfaceController.openParentApplication([NSObject : AnyObject](), reply: nil) 112 | } 113 | 114 | public override func passMessage(message: NSCoding, identifier: String) { 115 | wormhole.passMessageObject(message, identifier: identifier) 116 | 117 | WKInterfaceController.openParentApplication([NSObject : AnyObject](), reply: nil) 118 | } 119 | 120 | public override func stopListeningForMessage(#identifier: String) { 121 | wormhole.stopListeningForMessageWithIdentifier(identifier) 122 | } 123 | } 124 | 125 | #endif 126 | 127 | /// Stargate endpoint to be used on the Mac 128 | public class Earth : Base { 129 | public override init(applicationGroupIdentifier: String) { 130 | super.init(applicationGroupIdentifier: applicationGroupIdentifier) 131 | 132 | PeerKit.transceive(sanitizedIdentifier) 133 | passMessage(pingPayload, identifier: pingIdentifier) 134 | } 135 | 136 | public override func listenForMessage(#identifier: String, _ listener: ((AnyObject!) -> Void)) { 137 | PeerKit.eventBlocks[identifier] = { (peerID, object) -> Void in 138 | listener(object) 139 | } 140 | } 141 | 142 | public override func passMessage(message: NSCoding, identifier: String) { 143 | sendMultipeerMessage(message, identifier: identifier) 144 | } 145 | 146 | public override func stopListeningForMessage(#identifier: String) { 147 | PeerKit.stopTransceiving() 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'coveralls' 2 | Coveralls.wear! 3 | 4 | if ENV['CIRCLE_ARTIFACTS'] 5 | require 'simplecov' 6 | dir = File.join("..", "..", "..", ENV['CIRCLE_ARTIFACTS'], "coverage") 7 | SimpleCov.coverage_dir(dir) 8 | end 9 | 10 | require 'pathname' 11 | ROOT = Pathname.new(File.expand_path('../../', __FILE__)) 12 | $LOAD_PATH.unshift((ROOT + 'lib').to_s) 13 | $LOAD_PATH.unshift((ROOT + 'spec').to_s) 14 | 15 | require 'bundler/setup' 16 | require 'bacon' 17 | require 'mocha-on-bacon' 18 | require 'pretty_bacon' 19 | 20 | require 'grenouille' 21 | -------------------------------------------------------------------------------- /spec/swift_update_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../spec_helper', __FILE__) 2 | 3 | module Grenouille 4 | describe SwiftUpdate do 5 | it 'reports Swift 1.2 code correctly' do 6 | detected_version = SwiftUpdate.new.determine_version('spec/fixtures/Stargate.swift') 7 | 8 | detected_version.should == Gem::Version.new('1.2') 9 | end 10 | 11 | it 'reports Swift 1.1 code correctly' do 12 | detected_version = SwiftUpdate.new.determine_version('spec/fixtures/Blueprint.swift') 13 | 14 | detected_version.should == Gem::Version.new('1.1') 15 | end 16 | 17 | it 'can handle Swift 1.2 on iOS' do 18 | result = SwiftUpdate.new.update_to_latest_swift('spec/fixtures/Stargate.swift') 19 | 20 | result[:report].should == [] 21 | result[:output].should.start_with 'spec/fixtures/Stargate.swift:10:8: error: no such module \'PeerKit\'' 22 | end 23 | 24 | it 'can handle Swift 1.1 on iOS' do 25 | result = SwiftUpdate.new.update_to_latest_swift('spec/fixtures/Blueprint.swift') 26 | 27 | result[:report].count.should == 3 28 | result[:output].should == '' 29 | end 30 | 31 | it 'can handle Swift 1.2 on OS X' do 32 | result = SwiftUpdate.new.update_to_latest_swift('spec/fixtures/ChoreTask.swift') 33 | 34 | result[:report].should == [] 35 | result[:output].should == '' 36 | end 37 | 38 | it 'can handle Swift 1.1 on OS X' do 39 | result = SwiftUpdate.new.update_to_latest_swift('spec/fixtures/Mac-1.1.swift') 40 | 41 | result[:report].count.should == 1 42 | result[:output].should == '' 43 | end 44 | 45 | it 'supports file globbing' do 46 | detected_version = SwiftUpdate.new.determine_version('spec/fixtures/*.swift') 47 | 48 | detected_version.should == Gem::Version.new('1.1') 49 | end 50 | 51 | it 'accepts a list of files' do 52 | detected_version = SwiftUpdate.new.determine_version(['spec/fixtures/ChoresTask.swift', 'spec/fixtures/Mac-1.1.swift']) 53 | 54 | detected_version.should == Gem::Version.new('1.1') 55 | end 56 | 57 | it 'throws when non Swift files are provided as arguments' do 58 | should.raise(StandardError) { SwiftUpdate.new.determine_version('spec/*.rb') } 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /spec/xcode_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../spec_helper', __FILE__) 2 | 3 | module Grenouille 4 | describe Xcode do 5 | it 'can retrieve the version number of the currently selected Swift' do 6 | Xcode.any_instance.stubs(:xcrun).returns("Apple Swift version 1.3 (swiftlang-602.0.49.6 clang-602.0.49)\nTarget: x86_64-apple-darwin14.4.0") 7 | 8 | Xcode.new.swift_version.should == Gem::Version.new('1.3') 9 | end 10 | 11 | it 'can retrieve the version of Swift 1.1' do 12 | Xcode.any_instance.stubs(:xcrun).returns('Swift version 1.1 (swift-600.0.57.4)') 13 | 14 | Xcode.new.swift_version.should == Gem::Version.new('1.1') 15 | end 16 | 17 | it 'can retrieve the version number of the currently selected Xcode' do 18 | Xcode.any_instance.stubs(:xcrun).returns("Xcode 3.2.1\nBuild version 6D1002") 19 | 20 | Xcode.new.version.should == Liferaft::Version.new('6D1002') 21 | end 22 | 23 | it 'is resilient against xcodebuild not producing any output' do 24 | Xcode.any_instance.stubs(:xcrun).returns('') 25 | 26 | Xcode.new.version.should.be.nil 27 | end 28 | 29 | it 'is resilient against xcodebuild not producing the desired output' do 30 | Xcode.any_instance.stubs(:xcrun).returns(File.read(__FILE__)) 31 | 32 | Xcode.new.version.should.be.nil 33 | end 34 | end 35 | end 36 | --------------------------------------------------------------------------------