diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..2c7d170 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,7 @@ +version: 2 +updates: + # Maintain dependencies for GitHub Actions + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" diff --git a/.github/workflows/danger.yml b/.github/workflows/danger.yml new file mode 100644 index 0000000..322aefc --- /dev/null +++ b/.github/workflows/danger.yml @@ -0,0 +1,25 @@ +name: Danger + +on: + push: + branches: master + pull_request: + +jobs: + check: + name: Danger Check + runs-on: ubuntu-latest + steps: + - + name: Checkout + uses: actions/checkout@v3 + - + name: Setup Ruby + uses: ruby/setup-ruby@v1 + with: + bundler-cache: true + - + name: Run Danger + run: bundle exec danger --verbose --dangerfile=rakelib/Dangerfile + env: + DANGER_GITHUB_API_TOKEN: ${{ secrets.danger_github_api_token }} diff --git a/.github/workflows/lint-cocoapods.yml b/.github/workflows/lint-cocoapods.yml new file mode 100644 index 0000000..60e88e7 --- /dev/null +++ b/.github/workflows/lint-cocoapods.yml @@ -0,0 +1,23 @@ +name: Lint Cocoapods + +on: + push: + branches: master + pull_request: + +jobs: + lint: + name: Pod Lint + runs-on: ubuntu-latest + steps: + - + name: Checkout + uses: actions/checkout@v3 + - + name: Setup Ruby + uses: ruby/setup-ruby@v1 + with: + bundler-cache: true + - + name: Lint podspec + run: bundle exec rake pod:lint diff --git a/.github/workflows/release-check-versions.yml b/.github/workflows/release-check-versions.yml new file mode 100644 index 0000000..748c4df --- /dev/null +++ b/.github/workflows/release-check-versions.yml @@ -0,0 +1,23 @@ +name: Check Versions + +on: + push: + branches: + - 'release/**' + +jobs: + check_versions: + name: Check Versions + runs-on: ubuntu-latest + steps: + - + name: Checkout + uses: actions/checkout@v3 + - + name: Setup Ruby + uses: ruby/setup-ruby@v1 + with: + bundler-cache: true + - + name: Check versions + run: bundle exec rake release:check_versions diff --git a/.github/workflows/swiftlint.yml b/.github/workflows/swiftlint.yml new file mode 100644 index 0000000..0c2d1b7 --- /dev/null +++ b/.github/workflows/swiftlint.yml @@ -0,0 +1,26 @@ +name: SwiftLint + +on: + push: + branches: master + pull_request: + +jobs: + lint: + name: SwiftLint + runs-on: macos-latest + steps: + - + name: Checkout + uses: actions/checkout@v3 + - + name: Setup Ruby + uses: ruby/setup-ruby@v1 + with: + bundler-cache: true + - + name: Lint source code + run: bundle exec rake lint:code + - + name: Lint tests source code + run: bundle exec rake lint:tests diff --git a/.github/workflows/tag-publish.yml b/.github/workflows/tag-publish.yml new file mode 100644 index 0000000..99e7d66 --- /dev/null +++ b/.github/workflows/tag-publish.yml @@ -0,0 +1,44 @@ +name: Publish on Tag + +on: + push: + tags: + - '*' + workflow_dispatch: + +jobs: + cocoapods: + name: Push To CocoaPods + runs-on: ubuntu-latest + steps: + - + name: Checkout + uses: actions/checkout@v3 + - + name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + bundler-cache: true + - + name: Push to CocoaPods + run: bundle exec rake release:cocoapods + env: + COCOAPODS_TRUNK_TOKEN: ${{secrets.COCOAPODS_TRUNK_TOKEN}} + + github: + name: GitHub Release + runs-on: ubuntu-latest + steps: + - + name: Checkout + uses: actions/checkout@v3 + - + name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + bundler-cache: true + - + name: Create release on GitHub + run: bundle exec rake release:github + env: + DANGER_GITHUB_API_TOKEN: ${{ secrets.danger_github_api_token }} diff --git a/.github/workflows/test-spm.yml b/.github/workflows/test-spm.yml new file mode 100644 index 0000000..e647bb5 --- /dev/null +++ b/.github/workflows/test-spm.yml @@ -0,0 +1,66 @@ +name: Test SPM + +on: + push: + branches: master + pull_request: + +jobs: + linux: + name: Test SPM Linux + runs-on: ubuntu-latest + container: swiftgen/swift:5.6 + steps: + - + name: Checkout + uses: actions/checkout@v3 + - + # Note: we can't use `ruby/setup-ruby` on custom docker images, so we + # have to do our own caching + name: Cache gems + uses: actions/cache@v3 + with: + path: vendor/bundle + key: ${{ runner.os }}-gems-${{ hashFiles('Gemfile.lock') }} + restore-keys: | + ${{ runner.os }}-gems- + - + name: Cache SPM + uses: actions/cache@v3 + with: + path: .build + key: ${{ runner.os }}-spm-${{ hashFiles('Package.resolved') }} + restore-keys: | + ${{ runner.os }}-spm- + - + name: Bundle install + run: | + bundle config path vendor/bundle + bundle install --jobs 4 --retry 3 + - + name: Run tests + run: bundle exec rake spm:test + + macos: + name: Test SPM macOS + runs-on: macos-latest + steps: + - + name: Checkout + uses: actions/checkout@v3 + - + name: Setup Ruby + uses: ruby/setup-ruby@v1 + with: + bundler-cache: true + - + name: Cache SPM + uses: actions/cache@v3 + with: + path: .build + key: ${{ runner.os }}-spm-${{ hashFiles('Package.resolved') }} + restore-keys: | + ${{ runner.os }}-spm- + - + name: Run tests + run: bundle exec rake spm:test diff --git a/.gitignore b/.gitignore index 417edf6..4c2b92b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,75 @@ -.conche/ +# Xcode +# +# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore + +## Build generated +build/ +DerivedData/ +Fixtures/stub-env/**/*.swiftmodule + +## Various settings +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +xcuserdata/ + +## Other +*.moved-aside +*.xcuserstate +*.xcscmblueprint + +## Obj-C/Swift specific +*.hmap +*.ipa +*.dSYM.zip +*.dSYM + +## Playgrounds +timeline.xctimeline +playground.xcworkspace + +# Swift Package Manager +# +# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. +Packages/ .build/ .swiftpm/ -Packages/ -Package.pins -*.xcodeproj + +# CocoaPods +# +# We recommend against adding the Pods directory to your .gitignore. However +# you should judge for yourself, the pros and cons are mentioned at: +# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control +# +# Pods/ + +# Carthage +# +# Add this line if you want to avoid checking in source code from Carthage dependencies. +Carthage/Checkouts +Carthage/Build + +# fastlane +# +# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the +# screenshots whenever they are needed. +# For more information about the recommended setup visit: +# https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md + +fastlane/report.xml +fastlane/Preview.html +fastlane/screenshots +fastlane/test_output + +# Other stuff +.apitoken +.DS_Store +.idea/ +bin/ +Frameworks/ +Rome/ diff --git a/.ruby-version b/.ruby-version new file mode 100644 index 0000000..b0f2dcb --- /dev/null +++ b/.ruby-version @@ -0,0 +1 @@ +3.0.4 diff --git a/.swiftlint.yml b/.swiftlint.yml index 627f305..d227e2a 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -1,4 +1,4 @@ -swiftlint_version: 0.39.2 +swiftlint_version: 0.48.0 disabled_rules: # Remove this once we remove old swift support diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 871283e..0000000 --- a/.travis.yml +++ /dev/null @@ -1,22 +0,0 @@ -matrix: - include: - - os: osx - osx_image: xcode11.4 - env: SWIFT_VERSION=4.2 - - os: osx - osx_image: xcode11.4 - env: SWIFT_VERSION=5.0 - - os: linux - env: SWIFT_VERSION=4.2 - - os: linux - env: SWIFT_VERSION=5.0 -language: generic -sudo: required -dist: trusty -install: - - if [ "$TRAVIS_OS_NAME" == "linux" ]; then eval "$(curl -sL https://swiftenv.fuller.li/install.sh)"; fi - - if [ "$TRAVIS_OS_NAME" == "osx" ]; then wget --output-document /tmp/SwiftLint.pkg https://github.com/realm/SwiftLint/releases/download/0.39.2/SwiftLint.pkg && - sudo installer -pkg /tmp/SwiftLint.pkg -target /; fi -script: - - swift test - - if [ "$TRAVIS_OS_NAME" == "osx" ]; then swiftlint; fi diff --git a/CHANGELOG.md b/CHANGELOG.md index 61c9c05..e30b8c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,7 +20,9 @@ _None_ ### Internal Changes -_None_ +- Updated internal maintenance scripts, and switched to GitHub actions. + [David Jennes](https://github.com/djbe) + [#321](https://github.com/stencilproject/Stencil/pull/321) ## 0.14.2 @@ -179,7 +181,7 @@ _None_ - The `{% for %}` tag can now iterate over tuples, structures and classes via their stored properties. [Ilya Puchka](https://github.com/ilyapuchka) - [#172](https://github.com/stencilproject/Stencil/pull/173) + [#173](https://github.com/stencilproject/Stencil/pull/173) - Added `split` filter. [Ilya Puchka](https://github.com/ilyapuchka) [#187](https://github.com/stencilproject/Stencil/pull/187) @@ -288,7 +290,7 @@ _None_ ### Bug Fixes - You can now use literal filter arguments which contain quotes. - [#98](https://github.com/kylef/Stencil/pull/98) + [#98](https://github.com/stencilproject/Stencil/pull/98) ## 0.8.0 @@ -432,10 +434,10 @@ _None_ - Variables (`{{ variable.5 }}`) that reference an array index at an unknown index will now resolve to `nil` instead of causing a crash. - [#72](https://github.com/kylef/Stencil/issues/72) + [#72](https://github.com/stencilproject/Stencil/issues/72) - Templates can now extend templates that extend other templates. - [#60](https://github.com/kylef/Stencil/issues/60) + [#60](https://github.com/stencilproject/Stencil/issues/60) - If comparisons will now treat 0 and below numbers as negative. diff --git a/Gemfile b/Gemfile index 3fd311f..a51c7c8 100644 --- a/Gemfile +++ b/Gemfile @@ -1,7 +1,21 @@ # frozen_string_literal: true -source "https://rubygems.org" +source 'https://rubygems.org' -gem "octokit" -gem "cocoapods" -gem "rake" +# The bare minimum for building, e.g. in Homebrew +group :build do + gem 'rake', '~> 13.0' + gem 'xcpretty', '~> 0.3' +end + +# In addition to :build, for contributing +group :development do + gem 'cocoapods', '~> 1.11' + gem 'danger', '~> 8.4' + gem 'rubocop', '~> 1.22' +end + +# For releasing to GitHub +group :release do + gem 'octokit', '~> 4.7' +end diff --git a/Gemfile.lock b/Gemfile.lock index 4541abc..2398a92 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,9 +1,9 @@ GEM remote: https://rubygems.org/ specs: - CFPropertyList (3.0.4) + CFPropertyList (3.0.5) rexml - activesupport (6.1.4.1) + activesupport (6.1.6.1) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) minitest (>= 5.1) @@ -14,12 +14,17 @@ GEM algoliasearch (1.27.5) httpclient (~> 2.8, >= 2.8.3) json (>= 1.5.1) + ast (2.4.2) atomos (0.1.3) - claide (1.0.3) - cocoapods (1.11.2) + claide (1.1.0) + claide-plugins (0.9.2) + cork + nap + open4 (~> 1.3) + cocoapods (1.11.3) addressable (~> 2.8) claide (>= 1.0.2, < 2.0) - cocoapods-core (= 1.11.2) + cocoapods-core (= 1.11.3) cocoapods-deintegrate (>= 1.0.3, < 2.0) cocoapods-downloader (>= 1.4.0, < 2.0) cocoapods-plugins (>= 1.0.0, < 2.0) @@ -34,7 +39,7 @@ GEM nap (~> 1.0) ruby-macho (>= 1.0, < 3.0) xcodeproj (>= 1.21.0, < 2.0) - cocoapods-core (1.11.2) + cocoapods-core (1.11.3) activesupport (>= 5.0, < 7) addressable (~> 2.8) algoliasearch (~> 1.0) @@ -45,7 +50,7 @@ GEM public_suffix (~> 4.0) typhoeus (~> 1.0) cocoapods-deintegrate (1.0.5) - cocoapods-downloader (1.5.1) + cocoapods-downloader (1.6.3) cocoapods-plugins (1.0.0) nap cocoapods-search (1.0.1) @@ -54,74 +59,131 @@ GEM netrc (~> 0.11) cocoapods-try (1.2.0) colored2 (3.1.2) - concurrent-ruby (1.1.9) + concurrent-ruby (1.1.10) + cork (0.3.0) + colored2 (~> 3.1) + danger (8.6.1) + claide (~> 1.0) + claide-plugins (>= 0.9.2) + colored2 (~> 3.1) + cork (~> 0.1) + faraday (>= 0.9.0, < 2.0) + faraday-http-cache (~> 2.0) + git (~> 1.7) + kramdown (~> 2.3) + kramdown-parser-gfm (~> 1.0) + no_proxy_fix + octokit (~> 4.7) + terminal-table (>= 1, < 4) escape (0.0.4) ethon (0.15.0) ffi (>= 1.15.0) - faraday (1.8.0) + faraday (1.10.0) faraday-em_http (~> 1.0) faraday-em_synchrony (~> 1.0) faraday-excon (~> 1.1) - faraday-httpclient (~> 1.0.1) + faraday-httpclient (~> 1.0) + faraday-multipart (~> 1.0) faraday-net_http (~> 1.0) - faraday-net_http_persistent (~> 1.1) + faraday-net_http_persistent (~> 1.0) faraday-patron (~> 1.0) faraday-rack (~> 1.0) - multipart-post (>= 1.2, < 3) + faraday-retry (~> 1.0) ruby2_keywords (>= 0.0.4) faraday-em_http (1.0.0) faraday-em_synchrony (1.0.0) faraday-excon (1.1.0) + faraday-http-cache (2.4.0) + faraday (>= 0.8) faraday-httpclient (1.0.1) + faraday-multipart (1.0.4) + multipart-post (~> 2) faraday-net_http (1.0.1) faraday-net_http_persistent (1.2.0) faraday-patron (1.0.0) faraday-rack (1.0.0) - ffi (1.15.4) + faraday-retry (1.0.3) + ffi (1.15.5) fourflusher (2.3.1) fuzzy_match (2.0.4) gh_inspector (1.1.3) + git (1.11.0) + rchardet (~> 1.8) httpclient (2.8.3) - i18n (1.8.11) + i18n (1.12.0) concurrent-ruby (~> 1.0) - json (2.6.1) - minitest (5.14.4) + json (2.6.2) + kramdown (2.4.0) + rexml + kramdown-parser-gfm (1.1.0) + kramdown (~> 2.0) + minitest (5.16.2) molinillo (0.8.0) - multipart-post (2.1.1) + multipart-post (2.2.3) nanaimo (0.3.0) nap (1.1.0) netrc (0.11.0) - octokit (4.21.0) - faraday (>= 0.9) - sawyer (~> 0.8.0, >= 0.5.3) - public_suffix (4.0.6) + no_proxy_fix (0.1.2) + octokit (4.25.1) + faraday (>= 1, < 3) + sawyer (~> 0.9) + open4 (1.3.4) + parallel (1.22.1) + parser (3.1.2.0) + ast (~> 2.4.1) + public_suffix (4.0.7) + rainbow (3.1.1) rake (13.0.6) + rchardet (1.8.0) + regexp_parser (2.5.0) rexml (3.2.5) + rouge (2.0.7) + rubocop (1.32.0) + json (~> 2.3) + parallel (~> 1.10) + parser (>= 3.1.0.0) + rainbow (>= 2.2.2, < 4.0) + regexp_parser (>= 1.8, < 3.0) + rexml (>= 3.2.5, < 4.0) + rubocop-ast (>= 1.19.1, < 2.0) + ruby-progressbar (~> 1.7) + unicode-display_width (>= 1.4.0, < 3.0) + rubocop-ast (1.19.1) + parser (>= 3.1.1.0) ruby-macho (2.5.1) + ruby-progressbar (1.11.0) ruby2_keywords (0.0.5) - sawyer (0.8.2) + sawyer (0.9.2) addressable (>= 2.3.5) - faraday (> 0.8, < 2.0) + faraday (>= 0.17.3, < 3) + terminal-table (3.0.2) + unicode-display_width (>= 1.1.1, < 3) typhoeus (1.4.0) ethon (>= 0.9.0) - tzinfo (2.0.4) + tzinfo (2.0.5) concurrent-ruby (~> 1.0) - xcodeproj (1.21.0) + unicode-display_width (2.2.0) + xcodeproj (1.22.0) CFPropertyList (>= 2.3.3, < 4.0) atomos (~> 0.1.3) claide (>= 1.0.2, < 2.0) colored2 (~> 3.1) nanaimo (~> 0.3.0) rexml (~> 3.2.4) - zeitwerk (2.5.1) + xcpretty (0.3.0) + rouge (~> 2.0.7) + zeitwerk (2.6.0) PLATFORMS ruby DEPENDENCIES - cocoapods - octokit - rake + cocoapods (~> 1.11) + danger (~> 8.4) + octokit (~> 4.7) + rake (~> 13.0) + rubocop (~> 1.22) + xcpretty (~> 0.3) BUNDLED WITH - 2.1.4 + 2.2.33 diff --git a/Rakefile b/Rakefile index 3a2a392..b91598c 100755 --- a/Rakefile +++ b/Rakefile @@ -1,10 +1,50 @@ +#!/usr/bin/rake -PODSPEC_FILE = 'Stencil.podspec.json' -CHANGELOG_FILE = 'CHANGELOG.md' - -if ENV['BUNDLE_GEMFILE'].nil? - puts "\u{274C} Please use bundle exec" +unless defined?(Bundler) + puts 'Please use bundle exec to run the rake command' exit 1 end +require 'English' + +## [ Constants ] ############################################################## + +POD_NAME = 'Stencil' +MIN_XCODE_VERSION = 13.0 +BUILD_DIR = File.absolute_path('./.build') + +## [ Build Tasks ] ############################################################ + +namespace :files do + desc 'Update all files containing a version' + task :update, [:version] do |_, args| + version = args[:version] + + Utils.print_header "Updating files for version #{version}" + + podspec = Utils.podspec(POD_NAME) + podspec['version'] = version + podspec['source']['tag'] = version + File.write("#{POD_NAME}.podspec.json", JSON.pretty_generate(podspec) + "\n") + + replace('CHANGELOG.md', '## Master' => "\#\# #{version}") + replace("docs/conf.py", + /^version = .*/ => %Q(version = '#{version}'), + /^release = .*/ => %Q(release = '#{version}') + ) + replace("docs/installation.rst", + /pod 'Stencil', '.*'/ => %Q(pod 'Stencil', '~> #{version}'), + /github "stencilproject\/Stencil" ~> .*/ => %Q(github "stencilproject/Stencil" ~> #{version}) + ) + end + + def replace(file, replacements) + content = File.read(file) + replacements.each do |match, replacement| + content.gsub!(match, replacement) + end + File.write(file, content) + end +end + task :default => 'release:new' diff --git a/Sources/Extension.swift b/Sources/Extension.swift index a91b4ab..26bfae7 100644 --- a/Sources/Extension.swift +++ b/Sources/Extension.swift @@ -36,7 +36,7 @@ open class Extension { /// Registers a template filter with the given name public func registerFilter(_ name: String, filter: @escaping (Any?, [Any?]) throws -> Any?) { - filters[name] = .arguments({ value, args, _ in try filter(value, args) }) + filters[name] = .arguments { value, args, _ in try filter(value, args) } } /// Registers a template filter with the given name diff --git a/Sources/Inheritence.swift b/Sources/Inheritence.swift index fcacdf0..87e5896 100644 --- a/Sources/Inheritence.swift +++ b/Sources/Inheritence.swift @@ -35,10 +35,8 @@ class BlockContext { extension Collection { func any(_ closure: (Iterator.Element) -> Bool) -> Iterator.Element? { - for element in self { - if closure(element) { - return element - } + for element in self where closure(element) { + return element } return nil diff --git a/Sources/Parser.swift b/Sources/Parser.swift index 0181552..355e632 100644 --- a/Sources/Parser.swift +++ b/Sources/Parser.swift @@ -1,5 +1,5 @@ public func until(_ tags: [String]) -> ((TokenParser, Token) -> Bool) { - return { parser, token in + return { _, token in if let name = token.components.first { for tag in tags where name == tag { return true diff --git a/Sources/Variable.swift b/Sources/Variable.swift index 122fa4a..995706c 100644 --- a/Sources/Variable.swift +++ b/Sources/Variable.swift @@ -48,7 +48,8 @@ public struct Variable: Equatable, Resolvable { /// Resolve the variable in the given context public func resolve(_ context: Context) throws -> Any? { - if variable.count > 1 && ((variable.hasPrefix("'") && variable.hasSuffix("'")) || (variable.hasPrefix("\"") && variable.hasSuffix("\""))) { + if variable.count > 1 && + ((variable.hasPrefix("'") && variable.hasSuffix("'")) || (variable.hasPrefix("\"") && variable.hasSuffix("\""))) { // String literal return String(variable[variable.index(after: variable.startIndex) ..< variable.index(before: variable.endIndex)]) } diff --git a/Tests/StencilTests/.swiftlint.yml b/Tests/StencilTests/.swiftlint.yml index 46c1b62..5c99d60 100644 --- a/Tests/StencilTests/.swiftlint.yml +++ b/Tests/StencilTests/.swiftlint.yml @@ -1,3 +1,5 @@ +parent_config: ../../.swiftlint.yml + disabled_rules: # rule identifiers to exclude from running - type_body_length - file_length diff --git a/Tests/StencilTests/LexerSpec.swift b/Tests/StencilTests/LexerSpec.swift index 4eb7e6b..7961458 100644 --- a/Tests/StencilTests/LexerSpec.swift +++ b/Tests/StencilTests/LexerSpec.swift @@ -117,7 +117,7 @@ final class LexerTests: XCTestCase { } func testPerformance() throws { - let path = Path(#file as String) + ".." + "fixtures" + "huge.html" + let path = Path(#file as String) + ".." + "fixtures" + "huge.html" let content: String = try path.read() measure { diff --git a/Tests/StencilTests/VariableSpec.swift b/Tests/StencilTests/VariableSpec.swift index e25da2e..e834064 100644 --- a/Tests/StencilTests/VariableSpec.swift +++ b/Tests/StencilTests/VariableSpec.swift @@ -63,7 +63,7 @@ final class VariableTests: XCTestCase { let result = try variable.resolve(self.context) as? String try expect(result) == "name" } - + it("can resolve a string literal with one double quote") { let variable = Variable("\"") let result = try variable.resolve(self.context) as? String @@ -75,13 +75,13 @@ final class VariableTests: XCTestCase { let result = try variable.resolve(self.context) as? String try expect(result) == "name" } - + it("can resolve a string literal with one single quote") { let variable = Variable("'") let result = try variable.resolve(self.context) as? String try expect(result).to.beNil() } - + it("can resolve an integer literal") { let variable = Variable("5") let result = try variable.resolve(self.context) as? Int diff --git a/docs/installation.rst b/docs/installation.rst index 136ac16..19e8e21 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -14,7 +14,7 @@ dependencies inside ``Package.swift``. let package = Package( name: "MyApplication", dependencies: [ - .Package(url: "https://github.com/stencilproject/Stencil.git", majorVersion: 0, minor: 13), + .package(url: "https://github.com/stencilproject/Stencil.git", from: "0.14.2"), ] ) diff --git a/rakelib/Dangerfile b/rakelib/Dangerfile new file mode 100644 index 0000000..70aa50f --- /dev/null +++ b/rakelib/Dangerfile @@ -0,0 +1,97 @@ +# frozen_string_literal: true + +require_relative 'check_changelog' + +is_release = github.branch_for_head.start_with?('release/') +is_hotfix = github.branch_for_head.start_with?('hotfix/') + +################################################ +# Welcome message +markdown [ + "Hey 👋 I'm Eve, the friendly bot watching over Stencil 🤖", + 'Thanks a lot for your contribution!', + '', '---', '' +] + +need_fixes = [] + +################################################ +# Make it more obvious that a PR is a work in progress and shouldn't be merged yet +warn('PR is classed as Work in Progress') if github.pr_title.include? '[WIP]' + +# Note when there is a big PR +message('Big PR') if git.lines_of_code > 500 && !is_release + +################################################ +# Check for correct base branch +if is_release + message('This is a Release PR') + + require 'open3' + + stdout, _, status = Open3.capture3('bundle', 'exec', 'rake', 'changelog:check') + markdown [ + '', + '### ChangeLog check', + '', + stdout + ] + need_fixes << fail('Please fix the CHANGELOG errors') unless status.success? + + stdout, _, status = Open3.capture3('bundle', 'exec', 'rake', 'release:check_versions') + markdown [ + '', + '### Release version check', + '', + stdout + ] + need_fixes << fail('Please fix the versions inconsistencies') unless status.success? +elsif is_hotfix + message('This is a Hotfix PR') +end + +################################################ +# Check for a CHANGELOG entry +declared_trivial = github.pr_title.include? '#trivial' +has_changelog = git.modified_files.include?('CHANGELOG.md') +changelog_msg = '' +unless has_changelog || declared_trivial + repo_url = github.pr_json['head']['repo']['html_url'] + pr_title = github.pr_title + pr_title += '.' unless pr_title.end_with?('.') + pr_number = github.pr_json['number'] + pr_url = github.pr_json['html_url'] + pr_author = github.pr_author + pr_author_url = "https://github.com/#{pr_author}" + + need_fixes = fail("Please include a CHANGELOG entry to credit your work. \nYou can find it at [CHANGELOG.md](#{repo_url}/blob/#{github.branch_for_head}/CHANGELOG.md).") + + changelog_msg = <<-CHANGELOG_FORMAT.gsub(/^ *\|/, '') + |📝 We use the following format for CHANGELOG entries: + |``` + |* #{pr_title} + | [##{pr_number}](#{pr_url}) + | [@#{pr_author}](#{pr_author_url}) + |``` + |:bulb: Don't forget to end the line describing your changes by a period and two spaces. + CHANGELOG_FORMAT + # changelog_msg is printed during the "Encouragement message" section, see below +end + +changelog_warnings = check_changelog +unless changelog_warnings.empty? + need_fixes << warn('Found some warnings in CHANGELOG.md') + changelog_warnings.each do |warning| + warn(warning[:message], file: 'CHANGELOG.md', line: warning[:line]) + end +end + +################################################ +# Encouragement message +if need_fixes.empty? + markdown('Seems like everything is in order 👍 You did a good job here! 🤝') +else + markdown('Once you fix those tiny nitpickings above, we should be good to go! 🙌') + markdown(changelog_msg) unless changelog_msg.empty? + markdown('â„šī¸ _I will update this comment as you add new commits_') +end diff --git a/rakelib/changelog.rake b/rakelib/changelog.rake index 9a9f80d..41be31a 100644 --- a/rakelib/changelog.rake +++ b/rakelib/changelog.rake @@ -1,34 +1,56 @@ -NEW_CHANGELOG_SECTION = "## Master\n" + ['Breaking', 'Enhancements', 'Deprecations', 'Bug Fixes', 'Internal Changes'].map do |s| - <<~MARKDOWN +# frozen_string_literal: true - ### #{s} - - _None_ - MARKDOWN -end.join +# Used constants: +# _none_ -def changelog_first_section - content = [] - section_count = 0 - File.foreach(CHANGELOG_FILE) do |line| - section_count += 1 if line.start_with?('## ') - break if section_count > 1 - content.append(line) if section_count == 1 - end - content[1..].join -end +require_relative 'check_changelog' namespace :changelog do - # rake changelog:reset - desc "Add a new empty section at the top of the changelog and git push it" + desc 'Add the empty CHANGELOG entries after a new release' task :reset do - header "Reset CHANGELOG" - content = File.read(CHANGELOG_FILE) - new_content = NEW_CHANGELOG_SECTION + "\n" + content - File.write(CHANGELOG_FILE, new_content) + changelog = File.read('CHANGELOG.md') + abort('A Master entry already exists') if changelog =~ /^##\s*Master$/ + changelog.sub!(/^##[^#]/, "#{header}\\0") + File.write('CHANGELOG.md', changelog) + end - sh("git", "add", CHANGELOG_FILE) - sh("git", "commit", "-m", "Reset CHANGELOG") - sh("git", "push") + def header + <<-HEADER.gsub(/^\s*\|/, '') + |## Master + | + |### Breaking + | + |_None_ + | + |### Enhancements + | + |_None_ + | + |### Deprecations + | + |_None_ + | + |### Bug Fixes + | + |_None_ + | + |### Internal Changes + | + |_None_ + | + HEADER + end + + desc 'Check if links to issues and PRs use matching numbers between text & link' + task :check do + warnings = check_changelog + if warnings.empty? + puts "\u{2705} All entries seems OK (end with period + 2 spaces, correct links)" + else + puts "\u{274C} Some warnings were found:\n" + Array(warnings.map do |warning| + " - Line #{warning[:line]}: #{warning[:message]}" + end).join("\n") + exit 1 + end end end diff --git a/rakelib/check_changelog.rb b/rakelib/check_changelog.rb new file mode 100644 index 0000000..b70d588 --- /dev/null +++ b/rakelib/check_changelog.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +# This analyze the CHANGELOG.md file and report warnings on its content +# +# It checks: +# - if the description part of each entry ends with a period and two spaces +# - that all links to PRs & issues with format [#nn](repo_url/nn) are consistent +# (use the same number in the link title and URL) +# +# @return Array of Hashes with keys `:line` & `:message` for each element +# +def check_changelog + current_repo = File.basename(`git remote get-url origin`.chomp, '.git').freeze + slug_re = '([a-zA-Z]*/[a-zA-Z]*)' + links = %r{\[#{slug_re}?\#([0-9]+)\]\(https://github.com/#{slug_re}/(issues|pull)/([0-9]+)\)} + links_typos = %r{https://github.com/#{slug_re}/(issue|pulls)/([0-9]+)} + + all_warnings = [] + inside_entry = false + last_line_has_correct_ending = false + + File.readlines('CHANGELOG.md').each_with_index do |line, idx| + line.chomp! # Remove \n the end, it's easier for checks below + was_inside_entry = inside_entry + just_started_new_entry = line.start_with?('* ') + inside_entry = true if just_started_new_entry + inside_entry = false if /^ \[.*\]\(.*\)$/ =~ line # link-only line + + if was_inside_entry && !inside_entry && !last_line_has_correct_ending + # We just ended an entry's description by starting the links, but description didn't end with '. ' + # Note: entry descriptions can be on multiple lines, hence the need to wait for the next line + # to not be inside an entry to be able to consider the previous line as the end of entry description. + all_warnings.concat [ + { line: idx, message: 'Line describing your entry should end with a period and 2 spaces.' } + ] + end + # Store if current line has correct ending, for next iteration, so that if the next line isn't + # part of the entry description, we can check if previous line ends description correctly. + # Also, lines just linking to CHANGELOG to other repositories (StencilSwiftKit & Stencil mainly) + # should be considered as not needing the '. ' ending. + last_line_has_correct_ending = line.end_with?('. ') || line.end_with?('/CHANGELOG.md)') + + # Now, check that links [#nn](.../nn) have matching numbers in link title & URL + wrong_links = line.scan(links).reject do |m| + slug = m[0] || "stencilproject/#{current_repo}" + (slug == m[2]) && (m[1] == m[4]) + end + all_warnings.concat Array(wrong_links.map do |m| + link_text = "#{m[0]}##{m[1]}" + link_url = "#{m[2]}##{m[4]}" + { line: idx + 1, message: "Link text is #{link_text} but links points to #{link_url}." } + end) + + # Flag common typos in GitHub issue/PR URLs + typo_links = line.scan(links_typos) + all_warnings.concat Array(typo_links.map do |_| + { line: idx + 1, message: 'This looks like a GitHub link URL with a typo. Issue links should use `/issues/123` (plural) and PR links should use `/pull/123` (singular).' } + end) + end + all_warnings +end diff --git a/rakelib/lint.rake b/rakelib/lint.rake new file mode 100644 index 0000000..7aac215 --- /dev/null +++ b/rakelib/lint.rake @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +# Used constants: +# - BUILD_DIR + +namespace :lint do + SWIFTLINT = 'rakelib/lint.sh' + SWIFTLINT_VERSION = '0.48.0' + + task :install do |task| + next if check_version + + if OS.mac? + url = "https://github.com/realm/SwiftLint/releases/download/#{SWIFTLINT_VERSION}/portable_swiftlint.zip" + else + url = "https://github.com/realm/SwiftLint/releases/download/#{SWIFTLINT_VERSION}/swiftlint_linux.zip" + end + tmppath = '/tmp/swiftlint.zip' + destination = "#{BUILD_DIR}/swiftlint" + + Utils.run([ + %(curl -Lo #{tmppath} #{url}), + %(rm -rf #{destination}), + %(mkdir -p #{destination}), + %(unzip #{tmppath} -d #{destination}) + ], task) + end + + desc 'Lint the code' + task :code => :install do |task| + Utils.print_header 'Linting the code' + Utils.run(%(#{SWIFTLINT} sources), task) + end + + desc 'Lint the tests' + task :tests => :install do |task| + Utils.print_header 'Linting the unit test code' + Utils.run(%(#{SWIFTLINT} tests), task) + end + + def check_version + swiftlint = "#{BUILD_DIR}/swiftlint/swiftlint" + return false unless File.executable?(swiftlint) + + current = `#{swiftlint} version`.chomp + required = SWIFTLINT_VERSION.chomp + + current == required + end +end diff --git a/rakelib/lint.sh b/rakelib/lint.sh new file mode 100755 index 0000000..d1a2140 --- /dev/null +++ b/rakelib/lint.sh @@ -0,0 +1,35 @@ +#!/bin/bash + +PROJECT_DIR="${PROJECT_DIR:-`cd "$(dirname $0)/..";pwd`}" +SWIFTLINT="${PROJECT_DIR}/.build/swiftlint/swiftlint" +CONFIG="${PROJECT_DIR}/.swiftlint.yml" +if [ $CI ]; then + REPORTER="--reporter github-actions-logging" +else + REPORTER= +fi + +# possible paths +paths_sources="Sources" +paths_tests="Tests/StencilTests" + +# load selected group +if [ $# -gt 0 ]; then + key="$1" +else + echo "error: need group to lint." + exit 1 +fi + +selected_path=`eval echo '$'paths_$key` +if [ -z "$selected_path" ]; then + echo "error: need a valid group to lint." + exit 1 +fi + +SUB_CONFIG="${PROJECT_DIR}/${selected_path}/.swiftlint.yml" +if [ -f "$SUB_CONFIG" ]; then + "$SWIFTLINT" lint --strict --config "$SUB_CONFIG" $REPORTER "${PROJECT_DIR}/${selected_path}" +else + "$SWIFTLINT" lint --strict --config "$CONFIG" $REPORTER "${PROJECT_DIR}/${selected_path}" +fi diff --git a/rakelib/pod.rake b/rakelib/pod.rake index 4d68c77..f7444fc 100644 --- a/rakelib/pod.rake +++ b/rakelib/pod.rake @@ -1,21 +1,12 @@ -require 'json' +# frozen_string_literal: true -def current_pod_version - JSON.parse(File.read(PODSPEC_FILE))['version'] -end +# Used constants: +# - POD_NAME namespace :pod do - # rake pod:lint - desc "Lint the podspec" - task :lint do - header "Linting podspec" - sh("pod", "lib", "lint", PODSPEC_FILE) - end - - # rake pod:push - desc "Push the podspec to trunk" - task :push do - header "Pushing podspec to trunk" - sh("pod", "trunk", "push", PODSPEC_FILE) + desc 'Lint the Pod' + task :lint do |task| + Utils.print_header 'Linting the pod spec' + Utils.run(%(bundle exec pod lib lint "#{POD_NAME}.podspec.json" --quick), task) end end diff --git a/rakelib/release.rake b/rakelib/release.rake index 8c69a28..2c4a31b 100644 --- a/rakelib/release.rake +++ b/rakelib/release.rake @@ -1,67 +1,95 @@ +# frozen_string_literal: true + +# Used constants: +# - BUILD_DIR + require 'json' namespace :release do + desc 'Create a new release' + task :new => [:check_versions, :check_tag_and_ask_to_release, 'spm:test', :github, :cocoapods] - # rake release:new - desc "Ask for a version number and prepare a release PR for that version" - task :new do - info "Current version is: #{current_pod_version}" - print "What version do you want to release? " - new_version = STDIN.gets.chomp + desc 'Check if all versions from the podspecs and CHANGELOG match' + task :check_versions do + results = [] - Rake::Task['release:start'].invoke(new_version) - end - - # rake release:start[version] - desc "Start a release by creating a PR with the required changes to bump the version" - task :start, [:version] => ['release:create_branch', 'release:update_files', 'pod:lint', 'release:push_branch', 'github:create_release_pr', 'github:pull_master'] - - # rake release:finish[version] - desc "Finish a release after the PR has been merged, by tagging master and pushing to trunk" - task :finish => ['github:pull_master', 'github:tag', 'pod:push', 'github:create_release', 'changelog:reset'] - - - ### Helper tasks ### - - # rake release:create_branch[version] - task :create_branch, [:version] do |_, args| - branch = release_branch(args[:version]) - - header "Creating release branch" - sh("git", "checkout", "-b", branch) - end - - # rake release:update_files[version] - task :update_files, [:version] do |_, args| - version = args[:version] - - header "Updating files for version #{version}" - - podspec = JSON.parse(File.read(PODSPEC_FILE)) - podspec['version'] = version - podspec['source']['tag'] = version - File.write(PODSPEC_FILE, JSON.pretty_generate(podspec) + "\n") - - replace(CHANGELOG_FILE, '## Master' => "\#\# #{version}") - replace("docs/conf.py", - /^version = .*/ => %Q(version = '#{version}'), - /^release = .*/ => %Q(release = '#{version}') - ) - replace("docs/installation.rst", - /pod 'Stencil', '.*'/ => %Q(pod 'Stencil', '~> #{version}'), - /github "stencilproject\/Stencil" ~> .*/ => %Q(github "stencilproject/Stencil" ~> #{version}) + # Check if bundler is installed first, as we'll need it for the cocoapods task (and we prefer to fail early) + `which bundler` + results << Utils.table_result( + $CHILD_STATUS.success?, + 'Bundler installed', + 'Install bundler using `gem install bundler` and run `bundle install` first.' ) - ## Commit Changes - sh("git", "add", PODSPEC_FILE, CHANGELOG_FILE, "docs/*") - sh("git", "commit", "-m", "Version #{version}") + # Extract version from podspec + podspec = Utils::podspec(POD_NAME) + v = podspec['version'] + Utils.table_info("#{POD_NAME}.podspec", v) + + # Check podspec tag + podspec_tag = podspec['source']['tag'] + results << Utils.table_result(podspec_tag == v, 'Podspec version & tag equal', 'Update the `tag` in podspec') + + # Check docs config + docs_version = Utils.first_match_in_file('docs/conf.py', /version = '(.+)'/, 1) + docs_release = Utils.first_match_in_file('docs/conf.py', /release = '(.+)'/, 1) + results << Utils.table_result(docs_version == v,'Docs, version updated', 'Update the `version` in docs/conf.py') + results << Utils.table_result(docs_release == v, 'Docs, release updated', 'Update the `release` in docs/conf.py') + + # Check docs installation + docs_package = Utils.first_match_in_file('docs/installation.rst', /\.package\(url: .+ from: "(.+)"/, 1) + docs_cocoapods = Utils.first_match_in_file('docs/installation.rst', /pod 'Stencil', '~> (.+)'/, 1) + docs_carthage = Utils.first_match_in_file('docs/installation.rst', /github ".+\/Stencil" ~> (.+)/, 1) + results << Utils.table_result(docs_package == v, 'Docs, package updated', 'Update the package version in docs/installation.rst') + results << Utils.table_result(docs_cocoapods == v, 'Docs, cocoapods updated', 'Update the cocoapods version in docs/installation.rst') + results << Utils.table_result(docs_carthage == v, 'Docs, carthage updated', 'Update the carthage version in docs/installation.rst') + + # Check if entry present in CHANGELOG + changelog_entry = Utils.first_match_in_file('CHANGELOG.md', /^## #{Regexp.quote(v)}$/) + results << Utils.table_result(changelog_entry, 'CHANGELOG, Entry added', "Add an entry for #{v} in CHANGELOG.md") + + changelog_has_stable = system("grep -qi '^## Master' CHANGELOG.md") + results << Utils.table_result(!changelog_has_stable, 'CHANGELOG, No master', 'Remove section for master branch in CHANGELOG') + + exit 1 unless results.all? end - # rake release:push_branch[version] - task :push_branch, [:version] do |_, args| - branch = release_branch(args[:version]) + desc "Check tag and ask to release" + task :check_tag_and_ask_to_release do + results = [] + podspec_version = Utils.podspec_version(POD_NAME) - header "Pushing #{branch} to origin" - sh("git", "push", "-u", "origin", branch) + tag_set = !`git ls-remote --tags . refs/tags/#{podspec_version}`.empty? + results << Utils.table_result( + tag_set, + 'Tag pushed', + 'Please create a tag and push it' + ) + + exit 1 unless results.all? + + print "Release version #{podspec_version} [Y/n]? " + exit 2 unless STDIN.gets.chomp == 'Y' + end + + desc "Create a new GitHub release" + task :github do + require 'octokit' + + client = Utils.octokit_client + tag = Utils.top_changelog_version + body = Utils.top_changelog_entry + + raise 'Must be a valid version' if tag == 'Master' + + repo_name = File.basename(`git remote get-url origin`.chomp, '.git').freeze + puts "Pushing release notes for tag #{tag}" + client.create_release("stencilproject/#{repo_name}", tag, name: tag, body: body) + end + + desc "pod trunk push #{POD_NAME} to CocoaPods" + task :cocoapods do + Utils.print_header 'Pushing pod to CocoaPods Trunk' + sh "bundle exec pod trunk push #{POD_NAME}.podspec.json" end end diff --git a/rakelib/spm.rake b/rakelib/spm.rake new file mode 100644 index 0000000..53ce59f --- /dev/null +++ b/rakelib/spm.rake @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +# Used constants: +# _none_ + +namespace :spm do + desc 'Build using SPM' + task :build do |task| + Utils.print_header 'Compile using SPM' + Utils.run('swift build', task, xcrun: true) + end + + desc 'Run SPM Unit Tests' + task :test => :build do |task| + Utils.print_header 'Run the unit tests using SPM' + Utils.run('swift test --parallel', task, xcrun: true) + end +end diff --git a/rakelib/utils.rake b/rakelib/utils.rake index 6d4f7d9..aba8a06 100644 --- a/rakelib/utils.rake +++ b/rakelib/utils.rake @@ -1,28 +1,266 @@ -def colorize(string, *codes) +# frozen_string_literal: true + +# Used constants: +# - MIN_XCODE_VERSION + +require 'json' +require 'open3' +require 'pathname' + +# Utility functions to run Xcode commands, extract versionning info and logs messages +# +class Utils + COLUMN_WIDTHS = [45, 12].freeze + + ## [ Run commands ] ######################################################### + + # formatter types + # :xcpretty : through xcpretty and store in artifacts + # :raw : store in artifacts + # :to_string : run using backticks and return output + + # run a command using xcrun and xcpretty if applicable + def self.run(command, task, subtask = '', xcrun: false, formatter: :raw) + commands = if xcrun and OS.mac? + Array(command).map { |cmd| "#{version_select} xcrun #{cmd}" } + else + Array(command) + end + case formatter + when :xcpretty then xcpretty(commands, task, subtask) + when :raw then plain(commands, task, subtask) + when :to_string then `#{commands.join(' && ')}` + else raise "Unknown formatter '#{formatter}'" + end + end + + ## [ Convenience Helpers ] ################################################## + + def self.podspec(file) + JSON.parse(File.read("#{file}.podspec.json")) + end + + def self.podspec_version(file) + podspec_as_json(file)['version'] + end + + def self.pod_trunk_last_version(pod) + require 'yaml' + stdout, _, _ = Open3.capture3('bundle', 'exec', 'pod', 'trunk', 'info', pod) + stdout.sub!("\n#{pod}\n", '') + last_version_line = YAML.safe_load(stdout).first['Versions'].last + /^[0-9.]*/.match(last_version_line)[0] # Just the 'x.y.z' part + end + + def self.spm_own_version(dep) + dependencies = JSON.load(File.new('Package.resolved'))['object']['pins'] + dependencies.find { |d| d['package'] == dep }['state']['version'] + end + + def self.spm_resolved_version(dep) + dependencies = JSON.load(File.new('Package.resolved'))['object']['pins'] + dependencies.find { |d| d['package'] == dep }['state']['version'] + end + + def self.last_git_tag_version + `git describe --tags --abbrev=0`.strip + end + + def self.octokit_client + token = ENV['DANGER_GITHUB_API_TOKEN'] + token ||= File.exist?('.apitoken') && File.read('.apitoken') + token ||= File.exist?('../.apitoken') && File.read('../.apitoken') + Utils.print_error('No .apitoken file found') unless token + require 'octokit' + Octokit::Client.new(access_token: token) + end + + def self.top_changelog_version(changelog_file = 'CHANGELOG.md') + header, _, _ = Open3.capture3('grep', '-m', '1', '^## ', changelog_file) + header.gsub('## ', '').strip + end + + def self.top_changelog_entry(changelog_file = 'CHANGELOG.md') + tag = top_changelog_version + stdout, _, _ = Open3.capture3('sed', '-n', "/^## #{tag}$/,/^## /p", changelog_file) + stdout.gsub(/^## .*$/, '').strip + end + + def self.first_match_in_file(file, regexp, index = 0) + File.foreach(file) do |line| + m = regexp.match(line) + return m[index] if m + end + end + + ## [ Print info/errors ] #################################################### + + # print an info header + def self.print_header(str) + puts "== #{str.chomp} ==".format(:yellow, :bold) + end + + # print an info message + def self.print_info(str) + puts str.chomp.format(:green) + end + + # print an error message + def self.print_error(str) + puts str.chomp.format(:red) + end + + # format an info message in a 2 column table + def self.table_header(col1, col2) + puts "| #{col1.ljust(COLUMN_WIDTHS[0])} | #{col2.ljust(COLUMN_WIDTHS[1])} |" + puts "| #{'-' * COLUMN_WIDTHS[0]} | #{'-' * COLUMN_WIDTHS[1]} |" + end + + # format an info message in a 2 column table + def self.table_info(label, msg) + puts "| #{label.ljust(COLUMN_WIDTHS[0])} | 👉 #{msg.ljust(COLUMN_WIDTHS[1] - 4)} |" + end + + # format a result message in a 2 column table + def self.table_result(result, label, error_msg) + if result + puts "| #{label.ljust(COLUMN_WIDTHS[0])} | #{'✅'.ljust(COLUMN_WIDTHS[1] - 1)} |" + else + puts "| #{label.ljust(COLUMN_WIDTHS[0])} | ❌ - #{error_msg.ljust(COLUMN_WIDTHS[1] - 6)} |" + end + result + end + + ## [ Private helper functions ] ################################################## + + # run a command, pipe output through 'xcpretty' and store the output in CI artifacts + def self.xcpretty(cmd, task, subtask) + command = Array(cmd).join(' && \\' + "\n") + + if ENV['CI'] + Rake.sh %(set -o pipefail && (\\\n#{command} \\\n) | bundle exec xcpretty --color --report junit) + elsif system('which xcpretty > /dev/null') + Rake.sh %(set -o pipefail && (\\\n#{command} \\\n) | bundle exec xcpretty --color) + else + Rake.sh command + end + end + private_class_method :xcpretty + + # run a command and store the output in CI artifacts + def self.plain(cmd, task, subtask) + command = Array(cmd).join(' && \\' + "\n") + + if ENV['CI'] + if OS.mac? + Rake.sh %(set -o pipefail && (#{command})) + else + # dash on linux doesn't support `set -o` + Rake.sh %(/bin/bash -eo pipefail -c "#{command}") + end + else + Rake.sh command + end + end + private_class_method :plain + + # select the xcode version we want/support + def self.version_select + @version_select ||= compute_developer_dir(MIN_XCODE_VERSION) + end + private_class_method :version_select + + # Return the "DEVELOPER_DIR=..." prefix to use in order to point to the best Xcode version + # + # @param [String|Float|Gem::Requirement] version_req + # The Xcode version requirement. + # - If it's a Float, it's converted to a "~> x.y" requirement + # - If it's a String, it's converted to a Gem::Requirement as is + # @note If you pass a String, be sure to use "~> " in the string unless you really want + # to point to an exact, very specific version + # + def self.compute_developer_dir(version_req) + version_req = Gem::Requirement.new("~> #{version_req}") if version_req.is_a?(Float) + version_req = Gem::Requirement.new(version_req) unless version_req.is_a?(Gem::Requirement) + # if current Xcode already fulfills min version don't force DEVELOPER_DIR=... + current_xcode_version = `xcodebuild -version`.split("\n").first.match(/[0-9.]+/).to_s + return '' if version_req.satisfied_by? Gem::Version.new(current_xcode_version) + + supported_versions = all_xcode_versions.select { |app| version_req.satisfied_by?(app[:vers]) } + latest_supported_xcode = supported_versions.sort_by { |app| app[:vers] }.last + + # Check if it's at least the right version + if latest_supported_xcode.nil? + raise "\n[!!!] Requires Xcode #{version_req}, but we were not able to find it. " \ + "If it's already installed, either `xcode-select -s` to it, or update your Spotlight index " \ + "with 'mdimport /Applications/Xcode*'\n\n" + end + + %(DEVELOPER_DIR="#{latest_supported_xcode[:path]}/Contents/Developer") + end + private_class_method :compute_developer_dir + + # @return [Array] A list of { :vers => ... , :path => ... } hashes + # of all Xcodes found on the machine using Spotlight + def self.all_xcode_versions + xcodes = `mdfind "kMDItemCFBundleIdentifier = 'com.apple.dt.Xcode'"`.chomp.split("\n") + xcodes.map do |path| + { vers: Gem::Version.new(`mdls -name kMDItemVersion -raw "#{path}"`), path: path } + end + end + private_class_method :all_xcode_versions +end + +# OS detection +# +module OS + def OS.mac? + (/darwin/ =~ RUBY_PLATFORM) != nil + end + + def OS.linux? + OS.unix? and not OS.mac? + end +end + +# Colorization support for Strings +# +class String + # colorization + FORMATTING = { + # text styling + bold: 1, + faint: 2, + italic: 3, + underline: 4, + # foreground colors + black: 30, + red: 31, + green: 32, + yellow: 33, + blue: 34, + magenta: 35, + cyan: 36, + white: 37, + # background colors + bg_black: 40, + bg_red: 41, + bg_green: 42, + bg_yellow: 43, + bg_blue: 44, + bg_magenta: 45, + bg_cyan: 46, + bg_white: 47 + }.freeze + + # only enable formatting if terminal supports it if `tput colors`.chomp.to_i >= 8 - code = codes.join(';') - puts "\e[#{code}m" + string + "\e[0m" + def format(*styles) + styles.map { |s| "\e[#{FORMATTING[s]}m" }.join + self + "\e[0m" + end else - puts string + def format(*_styles) + self + end end end - -def header(title) - puts colorize("==> #{title}...", 1, 32) # bold, green -end - -def info(string) - puts colorize(string, 34) # blue -end - -def release_branch(version) - "release/#{version}" -end - -def replace(file, replacements) - content = File.read(file) - replacements.each do |match, replacement| - content.gsub!(match, replacement) - end - File.write(file, content) -end