Merge branch 'main' into all-contributors/add-laxmorek

This commit is contained in:
Pedro Piñera Buendía 2020-10-29 10:11:31 +01:00 committed by GitHub
commit e2a11e7975
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
645 changed files with 12130 additions and 89457 deletions

View File

@ -10,6 +10,88 @@
"name": "kalkwarf",
"avatar_url": "https://avatars1.githubusercontent.com/u/1033839?v=4",
"profile": "https://github.com/kalkwarf",
"contributions": [
"ideas",
"bug"
]
},
{
"login": "fortmarek",
"name": "Marek Fořt",
"avatar_url": "https://avatars0.githubusercontent.com/u/9371695?v=4",
"profile": "https://github.com/fortmarek",
"contributions": [
"ideas"
]
},
{
"login": "fortmarek",
"name": "Marek Fořt",
"avatar_url": "https://avatars0.githubusercontent.com/u/9371695?v=4",
"profile": "https://github.com/fortmarek",
"contributions": [
"ideas"
]
},
{
"login": "kwridan",
"name": "Kas",
"avatar_url": "https://avatars3.githubusercontent.com/u/11914919?v=4",
"profile": "http://www.matrixprojects.net",
"contributions": [
"code"
]
},
{
"login": "natanrolnik",
"name": "Natan Rolnik",
"avatar_url": "https://avatars3.githubusercontent.com/u/1164565?v=4",
"profile": "http://natanrolnik.me",
"contributions": [
"bug"
]
},
{
"login": "svastven",
"name": "svastven",
"avatar_url": "https://avatars0.githubusercontent.com/u/42235915?v=4",
"profile": "https://github.com/svastven",
"contributions": [
"ideas"
]
},
{
"login": "bhuemer",
"name": "Bernhard Huemer",
"avatar_url": "https://avatars2.githubusercontent.com/u/1212480?v=4",
"profile": "http://bhuemer.github.io",
"contributions": [
"ideas"
]
},
{
"login": "mollyIV",
"name": "Daniel Jankowski",
"avatar_url": "https://avatars0.githubusercontent.com/u/10795657?v=4",
"profile": "https://djankowski.dev",
"contributions": [
"ideas"
]
},
{
"login": "facumenzella",
"name": "Facundo Menzella",
"avatar_url": "https://avatars1.githubusercontent.com/u/1125252?v=4",
"profile": "https://github.com/facumenzella",
"contributions": [
"ideas"
]
},
{
"login": "eito",
"name": "Eric Ito",
"avatar_url": "https://avatars3.githubusercontent.com/u/775643?v=4",
"profile": "https://github.com/eito",
"contributions": [
"ideas"
]
@ -23,6 +105,33 @@
"code",
"ideas"
]
},
{
"login": "olejnjak",
"name": "Jakub Olejník",
"avatar_url": "https://avatars1.githubusercontent.com/u/3148214?v=4",
"profile": "https://github.com/olejnjak",
"contributions": [
"ideas"
]
},
{
"login": "lakpa",
"name": "ldindu",
"avatar_url": "https://avatars0.githubusercontent.com/u/389328?v=4",
"profile": "https://github.com/lakpa",
"contributions": [
"ideas"
]
},
{
"login": "gtsifrikas",
"name": "George Tsifrikas",
"avatar_url": "https://avatars2.githubusercontent.com/u/8904378?v=4",
"profile": "https://github.com/gtsifrikas",
"contributions": [
"blog"
]
}
],
"contributorsPerLine": 7,

View File

@ -4,7 +4,7 @@ name: Checks
on:
push:
branches:
- master
- main
paths:
- Sources/**/*
- Tests/**/*
@ -21,15 +21,18 @@ jobs:
swiftformat:
name: SwiftFormat
runs-on: macOS-latest
strategy:
matrix:
xcode: ['12.1']
steps:
- uses: actions/checkout@v1
- name: Select Xcode 11.5
run: sudo xcode-select -switch /Applications/Xcode_11.5.app
- name: Select Xcode
run: sudo xcode-select -switch /Applications/Xcode_${{ matrix.xcode }}.app
- uses: actions/setup-ruby@v1
with:
ruby-version: '2.x'
- name: Install Bundler 2.0.2
run: gem install bundler --version 2.0.2
- name: Install Bundler 2.1.4
run: gem install bundler --version 2.1.4
- name: Install Bundler dependencies
run: bundle install
- name: Run swiftformat
@ -37,15 +40,18 @@ jobs:
swiftlint:
name: Swiftlint
runs-on: macOS-latest
strategy:
matrix:
xcode: ['12.1']
steps:
- uses: actions/checkout@v1
- name: Select Xcode 11.5
run: sudo xcode-select -switch /Applications/Xcode_11.5.app
- name: Select Xcode
run: sudo xcode-select -switch /Applications/Xcode_${{ matrix.xcode }}.app
- uses: actions/setup-ruby@v1
with:
ruby-version: '2.x'
- name: Install Bundler 2.0.2
run: gem install bundler --version 2.0.2
- name: Install Bundler 2.1.4
run: gem install bundler --version 2.1.4
- name: Install Bundler dependencies
run: bundle install
- name: Run swiftlint

View File

@ -16,10 +16,13 @@ jobs:
test:
name: Test
runs-on: macOS-latest
strategy:
matrix:
xcode: ['12.1']
steps:
- uses: actions/checkout@v1
- name: Select Xcode 11.5
run: sudo xcode-select -switch /Applications/Xcode_11.5.app
- name: Select Xcode
run: sudo xcode-select -switch /Applications/Xcode_${{ matrix.xcode }}.app
- name: Build Package
working-directory: ./tools/fixturegen
run: swift build

37
.github/workflows/release.yml vendored Normal file
View File

@ -0,0 +1,37 @@
name: Release
on:
push:
tags:
- '*'
jobs:
release:
name: Release Tuist
runs-on: macos-latest
steps:
- uses: actions/checkout@v2
with:
ref: main
fetch-depth: 0
- uses: actions/setup-ruby@v1
with:
ruby-version: '2.x'
# - name: Install Bundler 2.1.4
# run: gem install bundler --version 2.1.4
# - uses: actions/cache@v2
# with:
# path: vendor/bundle
# key: ${{ runner.os }}-gems-${{ hashFiles('**/Gemfile.lock') }}
# restore-keys: |
# ${{ runner.os }}-gems-
# - name: Bundle install
# run: |
# bundle config path vendor/bundle
# bundle install --jobs 4 --retry 3
# - name: Select Xcode 11.5
# run: sudo xcode-select -switch /Applications/Xcode_11.5.app
# - name: Create a new release
# uses: fortmarek/tapestry-action@0.1.1
# env:
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@ -3,7 +3,7 @@ name: Tuist
on:
push:
branches:
- master
- main
paths:
- Sources/**/*
- Tests/**/*
@ -28,11 +28,18 @@ jobs:
runs-on: macOS-latest
strategy:
matrix:
xcode: ['11.5', '12_beta']
xcode: ['11.5', '12.1']
steps:
- uses: actions/checkout@v1
- name: Select Xcode
run: sudo xcode-select -switch /Applications/Xcode_${{ matrix.xcode }}.app
- uses: actions/cache@v2
name: 'Cache SPM dependencies'
with:
path: .build
key: ${{ runner.os }}-spm-${{ hashFiles('**/Package.resolved') }}
restore-keys: |
${{ runner.os }}-spm-
- name: Run tests
run: |
rm -rf .coverage
@ -43,11 +50,17 @@ jobs:
runs-on: macOS-latest
strategy:
matrix:
xcode: ['11.5']
xcode: ['11.5', '12.1']
steps:
- uses: actions/checkout@v1
- name: Select Xcode
run: sudo xcode-select -switch /Applications/Xcode_${{ matrix.xcode }}.app
- uses: actions/cache@v2
with:
path: .build
key: ${{ runner.os }}-spm-${{ hashFiles('**/Package.resolved') }}
restore-keys: |
${{ runner.os }}-spm-
- name: Build Tuist for release
run: swift build -c release --product tuist
- name: Build Tuistenv for release
@ -57,7 +70,7 @@ jobs:
runs-on: macOS-latest
strategy:
matrix:
xcode: ['11.5']
xcode: ['11.5', '12.1']
feature:
[
'generate-1',
@ -65,34 +78,90 @@ jobs:
'generate-3',
'generate-4',
'generate-5',
'generate-6',
'init',
'lint-project',
'lint-code',
'scaffold',
'up',
'build',
'test',
]
steps:
- uses: actions/checkout@v1
- name: Select Xcode
run: sudo xcode-select -switch /Applications/Xcode_${{ matrix.xcode }}.app
- uses: actions/cache@v2
name: 'Cache SPM dependencies'
with:
path: .build
key: ${{ runner.os }}-${{ matrix.xcode }}-spm-${{ hashFiles('**/Package.resolved') }}
restore-keys: |
${{ runner.os }}-spm-
- uses: actions/setup-ruby@v1
with:
ruby-version: '2.x'
- name: Install Bundler 2.0.2
run: gem install bundler --version 2.0.2
- name: Install Bundler dependencies
run: bundle install
- name: Install Bundler 2.1.4
run: gem install bundler --version 2.1.4
- uses: actions/cache@v2
with:
path: vendor/bundle
key: ${{ runner.os }}-gems-${{ hashFiles('**/Gemfile.lock') }}
restore-keys: |
${{ runner.os }}-gems-
- name: Bundle install
run: |
bundle config path vendor/bundle
bundle install --jobs 4 --retry 3
- name: Run tests
run: FEATURE=features/${{ matrix.feature }}.feature bundle exec rake features
cache_acceptance_tests:
name: Cache Acceptance tests (${{ matrix.feature }})
xcode_12_acceptance_tests:
name: Xcode 12 Acceptance tests (${{ matrix.feature }})
runs-on: macOS-latest
strategy:
matrix:
xcode: ['12_beta']
feature: ['cache']
xcode: ['12.1']
feature:
[
'generate-6',
'cache-xcframeworks',
'cache-frameworks',
'precompiled',
]
steps:
- uses: actions/checkout@v1
- name: Select Xcode
run: sudo xcode-select -switch /Applications/Xcode_${{ matrix.xcode }}.app
- uses: actions/cache@v2
name: 'Cache SPM dependencies'
with:
path: .build
key: ${{ runner.os }}-${{ matrix.xcode }}-spm-${{ hashFiles('**/Package.resolved') }}
restore-keys: |
${{ runner.os }}-spm-
- uses: actions/setup-ruby@v1
with:
ruby-version: '2.x'
- name: Install Bundler 2.1.4
run: gem install bundler --version 2.1.4
- uses: actions/cache@v2
with:
path: vendor/bundle
key: ${{ runner.os }}-gems-${{ hashFiles('**/Gemfile.lock') }}
restore-keys: |
${{ runner.os }}-gems-
- name: Bundle install
run: |
bundle config path vendor/bundle
bundle install --jobs 4 --retry 3
- name: Run tests
run: FEATURE=features/${{ matrix.feature }}.feature bundle exec rake features
upload:
if: github.ref == 'refs/heads/main'
name: Upload
runs-on: macOS-latest
strategy:
matrix:
xcode: ['12.1']
steps:
- uses: actions/checkout@v1
- name: Select Xcode
@ -100,28 +169,18 @@ jobs:
- uses: actions/setup-ruby@v1
with:
ruby-version: '2.x'
- name: Install Bundler 2.0.2
run: gem install bundler --version 2.0.2
- name: Install Bundler dependencies
run: bundle install
- name: Run tests
run: FEATURE=features/${{ matrix.feature }}.feature bundle exec rake features
upload:
if: github.ref == 'refs/heads/master'
name: Upload
runs-on: macOS-latest
steps:
- uses: actions/checkout@v1
- name: Select Xcode 11.4
run: sudo xcode-select -switch /Applications/Xcode_11.5.app
- uses: actions/setup-ruby@v1
- name: Install Bundler 2.1.4
run: gem install bundler --version 2.1.4
- uses: actions/cache@v2
with:
ruby-version: '2.x'
- name: Install Bundler 2.0.2
run: gem install bundler --version 2.0.2
- name: Install Bundler dependencies
run: bundle install
path: vendor/bundle
key: ${{ runner.os }}-gems-${{ hashFiles('**/Gemfile.lock') }}
restore-keys: |
${{ runner.os }}-gems-
- name: Bundle install
run: |
bundle config path vendor/bundle
bundle install --jobs 4 --retry 3
- name: Package build and upload it to GCS
run: |
bundle exec rake package_commit

View File

@ -14,10 +14,13 @@ jobs:
test:
name: Build
runs-on: macOS-latest
strategy:
matrix:
xcode: ['12.1']
steps:
- uses: actions/checkout@v1
- name: Select Xcode 11.5
run: sudo xcode-select -switch /Applications/Xcode_11.5.app
- name: Select Xcode
run: sudo xcode-select -switch /Applications/Xcode_${{ matrix.xcode }}.app
- name: Build Package
working-directory: ./tools/tuistbench
run: swift build

View File

@ -1 +0,0 @@
1.0.0-beta.4

View File

@ -4,6 +4,87 @@ Please, check out guidelines: https://keepachangelog.com/en/1.0.0/
## Next
### Added
- Allow specifying Development Region via new `developmentRegion` parameter in `Config`s GenerationOption. [#1062](https://github.com/tuist/tuist/pull/1867) by [@svastven](https://github.com/svastven).
- Require the `Config.swift` file to be in the Tuist directory [#693](https://github.com/tuist/tuist/issues/693) by [@mollyIV](https://github.com/mollyIV).
- Mapper for the caching logic to locate the built products directory [#1929](https://github.com/tuist/tuist/pull/1929) by [@pepibumur](https://github.com/pepibumur).
- Extended `BuildPhaseGenerator` to generate script build phases [#1932](https://github.com/tuist/tuist/pull/1932) by [@pepibumur](https://github.com/pepibumur).
- Extend the `TargetContentHasher` to account for the `Target.scripts` attribute [#1933](https://github.com/tuist/tuist/pull/1933) by [@pepibumur](https://github.com/pepibumur).
- Extend the `CacheController` to generate projects with the build phase to locate the targets' built products directory [#1933](https://github.com/tuist/tuist/pull/1933) by [@pepibumur](https://github.com/pepibumur).
- Add support for appClip [#1854](https://github.com/tuist/tuist/pull/1854) by [@lakpa](https://github.com/lakpa).
### Fixed
- Fixed non-framework/library targets having a header build phase [#367](https://github.com/tuist/tuist/issues/367) by [@eito](https://github.com/eito).
- Fixed missing profile scheme arguments when specified in manifest [#1543](https://github.com/tuist/tuist/issues/1543) by [@lakpa](https://github.com/lakpa).
- Fixed cache warming exporting unrelated .frameworks [#1939](https://github.com/tuist/tuist/pull/1939) by [@pepibumur](https://github.com/pepibumur).
- Fixed cache warming building from a clean state for every target [#1939](https://github.com/tuist/tuist/pull/1939) by [@pepibumur](https://github.com/pepibumur).
### Changed
- Some renames in the generation logic to make the generation logic easier to reason about [#1942](https://github.com/tuist/tuist/pull/1942) by [@pepibumur](https://github.com/pepibumur).
- Update some Swift dependencies [#1971](https://github.com/tuist/tuist/pull/1971) by [@pepibumur](https://github.com/pepibumur).
## 1.22.0 - Heimat
### Changed
- Autogenerated `xxx-Project` scheme is now shared [#1902](https://github.com/tuist/tuist/pull/1902) by [@fortmarek](https://github.com/fortmarek)
### Added
- Allow build phase scripts to disable dependency analysis [#1883](https://github.com/tuist/tuist/pull/1883) by [@bhuemer](https://github.com/bhuemer).
- The default generated project does not include a LaunchScreen storyboard [#265](https://github.com/tuist/tuist/issues/265) by [@mollyIV](https://github.com/mollyIV).
## 1.21.0 - PBWerk
### Added
- Allow ignoring cache when running tuist focus [#1879](https://github.com/tuist/tuist/pull/1879) by [@natanrolnik](https://github.com/natanrolnik).
### Changed
- Improve error message to have more actionable information [#921](https://github.com/tuist/tuist/issues/921) by [@mollyIV](https://github.com/mollyIV).
### Fixed
- Fix calculation of Settings hash related to Cache commands [#1869](Fix calculation of Settings hash related to Cache commands) by [@natanrolnik](https://github.com/natanrolnik)
- Fixed handling of `.tuist_version` file if the file had a trailing line break [#1900](Allow trailing line break in `.tuist_version`) by [@kalkwarf](https://github.com/kalkwarf)
## 1.20.0 - Heideberg
### Changed
- Revert using root `.package.resolved` [#1830](https://github.com/tuist/tuist/pull/1830) by [@fortmarek](https://github.com/fortmarek)
### Added
- Support for caching frameworks instead of xcframeworks [#1851](https://github.com/tuist/tuist/pull/1851)
### Fixed
- Skip synthesizing resource accessors when the file/folder is empty [#1829](https://github.com/tuist/tuist/pull/1829) by [@fortmarek](https://github.com/fortmarek)
- The redirect after the cloud authentication is not being captured from the CLI [#1846](https://github.com/tuist/tuist/pull/1846) by [@pepibumur](https://github.com/pepibumur).
## 1.19.0 - Milano
### Fixed
- Ensure `DEVELOPER_DIR` is used in all `swiftc` calls [#1819](https://github.com/tuist/tuist/pull/1819) by [@kwridan](https://github.com/kwridan)
- Fixed decoding bug on DefaultSettings [#1817](https://github.com/tuist/tuist/issues/1817) by [@jakeatoms](https://github.com/jakeatoms)
- Bool compiler error when generating accessor for plists [#1827](https://github.com/tuist/tuist/pull/1827) by [@fortmarek](https://github.com/fortmarek)
### Added
- Add Workspace Mappers [#1767](https://github.com/tuist/tuist/pull/1767) by [@kwridan](https://github.com/kwridan)
- Extended `Config`'s generationOptions with `.disableShowEnvironmentVarsInScriptPhases`. It does what you'd think. [#1782](https://github.com/tuist/tuist/pull/1782) by [@kalkwarf](https://github.com/kalkwarf)
- Generate `xxx-Project` scheme to build and test all available targets by [#1765](https://github.com/tuist/tuist/pull/1765) by [@fortmarek](https://github.com/fortmarek)
### Changed
- The `tuist edit` command adds `Setup.swift` and `Config.swift` to the generated project if they exist. [#1745](https://github.com/tuist/tuist/pull/1745) by [@laxmorek](https://github.com/laxmorek)
## 1.18.1 - Manaslu
### Fixed

12
Gemfile
View File

@ -2,16 +2,16 @@
source "https://rubygems.org"
gem "cucumber", "~> 5.1"
gem "cucumber", "~> 5.2"
gem "rake", "~> 13.0"
gem "byebug", "~> 11.1"
gem "minitest", "~> 5.14"
gem "simctl", "~> 1.6"
gem "rubocop", "~> 0.90.0"
gem "rubocop", "~> 1.0.0"
gem "encrypted-environment", "~> 0.2.0"
gem "google-cloud-storage", "~> 1.28"
gem "google-cloud-storage", "~> 1.29"
gem "colorize", "~> 0.8.1"
gem "cocoapods", "~> 1.9"
gem "xcodeproj", "~> 1.18"
gem "cocoapods", "~> 1.10"
gem "xcodeproj", "~> 1.19"
gem "highline", "~> 2.0"
gem "zip", "~> 2.0.2"
gem "rubyzip", "~> 2.3.0"

View File

@ -2,14 +2,14 @@ GEM
remote: https://rubygems.org/
specs:
CFPropertyList (3.0.2)
activesupport (4.2.11.3)
i18n (~> 0.7)
activesupport (5.2.4.4)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 0.7, < 2)
minitest (~> 5.1)
thread_safe (~> 0.3, >= 0.3.4)
tzinfo (~> 1.1)
addressable (2.7.0)
public_suffix (>= 2.0.2, < 5.0)
algoliasearch (1.27.2)
algoliasearch (1.27.4)
httpclient (~> 2.8, >= 2.8.3)
json (>= 1.5.1)
ast (2.4.1)
@ -17,15 +17,14 @@ GEM
builder (3.2.4)
byebug (11.1.3)
claide (1.0.3)
cocoapods (1.9.3)
activesupport (>= 4.0.2, < 5)
cocoapods (1.10.0)
addressable (~> 2.6)
claide (>= 1.0.2, < 2.0)
cocoapods-core (= 1.9.3)
cocoapods-core (= 1.10.0)
cocoapods-deintegrate (>= 1.0.3, < 2.0)
cocoapods-downloader (>= 1.2.2, < 2.0)
cocoapods-downloader (>= 1.4.0, < 2.0)
cocoapods-plugins (>= 1.0.0, < 2.0)
cocoapods-search (>= 1.0.0, < 2.0)
cocoapods-stats (>= 1.0.0, < 2.0)
cocoapods-trunk (>= 1.4.0, < 2.0)
cocoapods-try (>= 1.1.0, < 2.0)
colored2 (~> 3.1)
@ -35,21 +34,22 @@ GEM
molinillo (~> 0.6.6)
nap (~> 1.0)
ruby-macho (~> 1.4)
xcodeproj (>= 1.14.0, < 2.0)
cocoapods-core (1.9.3)
activesupport (>= 4.0.2, < 6)
xcodeproj (>= 1.19.0, < 2.0)
cocoapods-core (1.10.0)
activesupport (> 5.0, < 6)
addressable (~> 2.6)
algoliasearch (~> 1.0)
concurrent-ruby (~> 1.1)
fuzzy_match (~> 2.0.4)
nap (~> 1.0)
netrc (~> 0.11)
public_suffix
typhoeus (~> 1.0)
cocoapods-deintegrate (1.0.4)
cocoapods-downloader (1.3.0)
cocoapods-downloader (1.4.0)
cocoapods-plugins (1.0.0)
nap
cocoapods-search (1.0.0)
cocoapods-stats (1.1.0)
cocoapods-trunk (1.5.0)
nap (>= 0.8, < 2.0)
netrc (~> 0.11)
@ -57,14 +57,14 @@ GEM
colored2 (3.1.2)
colorize (0.8.1)
concurrent-ruby (1.1.7)
cucumber (5.1.1)
cucumber (5.2.0)
builder (~> 3.2, >= 3.2.4)
cucumber-core (~> 8.0, >= 8.0.1)
cucumber-create-meta (~> 2.0, >= 2.0.2)
cucumber-cucumber-expressions (~> 10.3, >= 10.3.0)
cucumber-gherkin (~> 15.0, >= 15.0.2)
cucumber-html-formatter (~> 9.0, >= 9.0.0)
cucumber-messages (~> 13.0, >= 13.0.1)
cucumber-messages (~> 13.1, >= 13.1.0)
cucumber-wire (~> 4.0, >= 4.0.1)
diff-lcs (~> 1.4, >= 1.4.4)
multi_test (~> 0.1, >= 0.1.2)
@ -81,7 +81,7 @@ GEM
cucumber-messages (~> 13.0, >= 13.0.1)
cucumber-html-formatter (9.0.0)
cucumber-messages (~> 13.0, >= 13.0.1)
cucumber-messages (13.0.1)
cucumber-messages (13.1.0)
protobuf-cucumber (~> 3.10, >= 3.10.8)
cucumber-tag-expressions (2.0.4)
cucumber-wire (4.0.1)
@ -105,13 +105,14 @@ GEM
fourflusher (2.3.1)
fuzzy_match (2.0.4)
gh_inspector (1.1.3)
google-api-client (0.43.0)
google-api-client (0.46.1)
addressable (~> 2.5, >= 2.5.1)
googleauth (~> 0.9)
httpclient (>= 2.8.1, < 3.0)
mini_mime (~> 1.0)
representable (~> 3.0)
retriable (>= 2.0, < 4.0)
rexml
signet (~> 0.12)
google-cloud-core (1.5.0)
google-cloud-env (~> 1.0)
@ -119,14 +120,14 @@ GEM
google-cloud-env (1.3.3)
faraday (>= 0.17.3, < 2.0)
google-cloud-errors (1.0.1)
google-cloud-storage (1.28.0)
google-cloud-storage (1.29.1)
addressable (~> 2.5)
digest-crc (~> 0.4)
google-api-client (~> 0.33)
google-cloud-core (~> 1.2)
googleauth (~> 0.9)
mini_mime (~> 1.0)
googleauth (0.13.1)
googleauth (0.14.0)
faraday (>= 0.17.3, < 2.0)
jwt (>= 1.4, < 3.0)
memoist (~> 0.16)
@ -135,9 +136,9 @@ GEM
signet (~> 0.14)
highline (2.0.3)
httpclient (2.8.3)
i18n (0.9.5)
i18n (1.8.5)
concurrent-ruby (~> 1.0)
json (2.3.0)
json (2.3.1)
jwt (2.2.2)
memoist (0.16.2)
middleware (0.1.0)
@ -153,36 +154,37 @@ GEM
netrc (0.11.0)
os (1.1.1)
parallel (1.19.2)
parser (2.7.1.4)
parser (2.7.2.0)
ast (~> 2.4.1)
protobuf-cucumber (3.10.8)
activesupport (>= 3.2)
middleware
thor
thread_safe
public_suffix (4.0.5)
public_suffix (4.0.6)
rainbow (3.0.0)
rake (13.0.1)
regexp_parser (1.7.1)
regexp_parser (1.8.2)
representable (3.0.4)
declarative (< 0.1.0)
declarative-option (< 0.2.0)
uber (< 0.2.0)
retriable (3.1.2)
rexml (3.2.4)
rubocop (0.90.0)
rubocop (1.0.0)
parallel (~> 1.10)
parser (>= 2.7.1.1)
parser (>= 2.7.1.5)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 1.7)
regexp_parser (>= 1.8)
rexml
rubocop-ast (>= 0.3.0, < 1.0)
rubocop-ast (>= 0.6.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 1.4.0, < 2.0)
rubocop-ast (0.3.0)
parser (>= 2.7.1.4)
rubocop-ast (1.0.1)
parser (>= 2.7.1.5)
ruby-macho (1.4.0)
ruby-progressbar (1.10.1)
rubyzip (2.3.0)
signet (0.14.0)
addressable (~> 2.3)
faraday (>= 0.17.3, < 2.0)
@ -201,31 +203,30 @@ GEM
thread_safe (~> 0.1)
uber (0.1.0)
unicode-display_width (1.7.0)
xcodeproj (1.18.0)
xcodeproj (1.19.0)
CFPropertyList (>= 2.3.3, < 4.0)
atomos (~> 0.1.3)
claide (>= 1.0.2, < 2.0)
colored2 (~> 3.1)
nanaimo (~> 0.3.0)
zip (2.0.2)
PLATFORMS
ruby
DEPENDENCIES
byebug (~> 11.1)
cocoapods (~> 1.9)
cocoapods (~> 1.10)
colorize (~> 0.8.1)
cucumber (~> 5.1)
cucumber (~> 5.2)
encrypted-environment (~> 0.2.0)
google-cloud-storage (~> 1.28)
google-cloud-storage (~> 1.29)
highline (~> 2.0)
minitest (~> 5.14)
rake (~> 13.0)
rubocop (~> 0.90.0)
rubocop (~> 1.0.0)
rubyzip (~> 2.3.0)
simctl (~> 1.6)
xcodeproj (~> 1.18)
zip (~> 2.0.2)
xcodeproj (~> 1.19)
BUNDLED WITH
2.1.2
2.1.4

View File

@ -150,8 +150,8 @@
"repositoryURL": "https://github.com/apple/swift-argument-parser",
"state": {
"branch": null,
"revision": "7255fd547f70468e19abbac5f7964f1ef309ad92",
"version": "0.2.1"
"revision": "92646c0cdbaca076c8d3d0207891785b3379cbff",
"version": "0.3.1"
}
},
{
@ -168,8 +168,8 @@
"repositoryURL": "https://github.com/apple/swift-tools-support-core",
"state": {
"branch": null,
"revision": "f39fc6c12266697b1585589090f0004903974685",
"version": "0.1.10"
"revision": "243beea77d20db46647a3de4765c96e2c801c7c7",
"version": "0.1.12"
}
},
{
@ -177,8 +177,8 @@
"repositoryURL": "https://github.com/httpswift/swifter.git",
"state": {
"branch": null,
"revision": "8b5afb48ae64d4f729f0489ddcfe09c62b9c3687",
"version": "1.4.7"
"revision": "9483a5d459b45c3ffd059f7b55f9638e268632fd",
"version": "1.5.0"
}
},
{
@ -195,8 +195,8 @@
"repositoryURL": "https://github.com/thii/xcbeautify.git",
"state": {
"branch": null,
"revision": "77bf4eae0f090dbf7cbcabb68fcaf0e7ff9826db",
"version": "0.8.0"
"revision": "fd7b0b6972809eead52b9016b383cf6d467e00b0",
"version": "0.8.1"
}
},
{
@ -204,8 +204,8 @@
"repositoryURL": "https://github.com/tuist/XcodeProj",
"state": {
"branch": null,
"revision": "81bb2bb333eafa68f8ecd8187a4bb56d51e78e97",
"version": "7.14.0"
"revision": "2ae8e322ddbed99655802533441bc52f9ae76f24",
"version": "7.17.0"
}
},
{
@ -231,8 +231,8 @@
"repositoryURL": "https://github.com/marmelroy/Zip.git",
"state": {
"branch": null,
"revision": "80b1c3005ee25b4c7ce46c4029ac3347e8d5e37e",
"version": "2.0.0"
"revision": "bd19d974e8a38cc8d3a88c90c8a107386c3b8ccf",
"version": "2.1.1"
}
}
]

View File

@ -27,19 +27,19 @@ let package = Package(
targets: ["TuistGenerator"]),
],
dependencies: [
.package(url: "https://github.com/tuist/XcodeProj", .upToNextMajor(from: "7.11.0")),
.package(url: "https://github.com/tuist/XcodeProj", .upToNextMajor(from: "7.17.0")),
.package(url: "https://github.com/IBM-Swift/BlueSignals", .upToNextMajor(from: "1.0.21")),
.package(url: "https://github.com/ReactiveX/RxSwift.git", .upToNextMajor(from: "5.0.1")),
.package(url: "https://github.com/ReactiveX/RxSwift.git", .upToNextMajor(from: "5.1.1")),
.package(url: "https://github.com/rnine/Checksum.git", .upToNextMajor(from: "1.0.2")),
.package(url: "https://github.com/apple/swift-log.git", .upToNextMajor(from: "1.2.0")),
.package(url: "https://github.com/thii/xcbeautify.git", .upToNextMajor(from: "0.8.0")),
.package(url: "https://github.com/apple/swift-log.git", .upToNextMajor(from: "1.4.0")),
.package(url: "https://github.com/thii/xcbeautify.git", .upToNextMajor(from: "0.8.1")),
.package(url: "https://github.com/krzyzanowskim/CryptoSwift", .upToNextMajor(from: "1.3.0")),
.package(url: "https://github.com/stencilproject/Stencil", .branch("master")),
.package(url: "https://github.com/kishikawakatsumi/KeychainAccess.git", .upToNextMajor(from: "4.1.0")),
.package(url: "https://github.com/httpswift/swifter.git", .upToNextMajor(from: "1.4.7")),
.package(url: "https://github.com/apple/swift-tools-support-core", .upToNextMinor(from: "0.1.1")),
.package(url: "https://github.com/apple/swift-argument-parser", .upToNextMajor(from: "0.0.6")),
.package(url: "https://github.com/marmelroy/Zip.git", .upToNextMinor(from: "2.0.0")),
.package(url: "https://github.com/httpswift/swifter.git", .upToNextMajor(from: "1.5.0")),
.package(url: "https://github.com/apple/swift-tools-support-core", .upToNextMinor(from: "0.1.12")),
.package(url: "https://github.com/apple/swift-argument-parser", .upToNextMajor(from: "0.3.1")),
.package(url: "https://github.com/marmelroy/Zip.git", .upToNextMinor(from: "2.1.1")),
.package(url: "https://github.com/tuist/GraphViz.git", .branch("tuist")),
.package(url: "https://github.com/fortmarek/SwiftGen", .branch("stable")),
.package(url: "https://github.com/fortmarek/StencilSwiftKit.git", .branch("stable")),
@ -75,11 +75,11 @@ let package = Package(
),
.target(
name: "TuistKit",
dependencies: ["XcodeProj", "SwiftToolsSupport-auto", "ArgumentParser", "TuistSupport", "TuistGenerator", "TuistCache", "TuistAutomation", "ProjectDescription", "Signals", "RxSwift", "RxBlocking", "Checksum", "TuistLoader", "TuistInsights", "TuistScaffold", "TuistSigning", "TuistCloud", "TuistDoc", "GraphViz", "TuistMigration"]
dependencies: ["XcodeProj", "SwiftToolsSupport-auto", "ArgumentParser", "TuistSupport", "TuistGenerator", "TuistCache", "TuistAutomation", "ProjectDescription", "Signals", "RxSwift", "RxBlocking", "Checksum", "TuistLoader", "TuistInsights", "TuistScaffold", "TuistSigning", "TuistDependencies", "TuistCloud", "TuistDoc", "GraphViz", "TuistMigration"]
),
.testTarget(
name: "TuistKitTests",
dependencies: ["TuistKit", "TuistAutomation", "TuistSupportTesting", "TuistCoreTesting", "ProjectDescription", "RxBlocking", "TuistLoaderTesting", "TuistCacheTesting", "TuistGeneratorTesting", "TuistScaffoldTesting", "TuistCloudTesting", "TuistAutomationTesting", "TuistSigningTesting", "TuistMigrationTesting", "TuistDocTesting"]
dependencies: ["TuistKit", "TuistAutomation", "TuistSupportTesting", "TuistCoreTesting", "ProjectDescription", "RxBlocking", "TuistLoaderTesting", "TuistCacheTesting", "TuistGeneratorTesting", "TuistScaffoldTesting", "TuistCloudTesting", "TuistAutomationTesting", "TuistSigningTesting", "TuistDependenciesTesting", "TuistMigrationTesting", "TuistDocTesting"]
),
.testTarget(
name: "TuistKitIntegrationTests",
@ -151,7 +151,7 @@ let package = Package(
),
.target(
name: "TuistCacheTesting",
dependencies: ["TuistCache", "SwiftToolsSupport-auto", "TuistCore", "RxTest", "RxSwift"]
dependencies: ["TuistCache", "SwiftToolsSupport-auto", "TuistCore", "RxTest", "RxSwift", "TuistSupportTesting"]
),
.target(
name: "TuistCloud",
@ -229,6 +229,22 @@ let package = Package(
name: "TuistSigningIntegrationTests",
dependencies: ["TuistSigning", "TuistSupportTesting", "TuistCoreTesting", "TuistSigningTesting"]
),
.target(
name: "TuistDependencies",
dependencies: ["TuistCore", "TuistSupport"]
),
.target(
name: "TuistDependenciesTesting",
dependencies: ["TuistDependencies"]
),
.testTarget(
name: "TuistDependenciesTests",
dependencies: ["TuistDependencies", "TuistDependenciesTesting", "TuistCoreTesting", "TuistSupportTesting"]
),
.testTarget(
name: "TuistDependenciesIntegrationTests",
dependencies: ["TuistDependencies", "TuistDependenciesTesting", "TuistCoreTesting", "TuistSupportTesting"]
),
.target(
name: "TuistMigration",
dependencies: ["TuistCore", "TuistSupport", "XcodeProj", "SwiftToolsSupport-auto"]

View File

@ -1,8 +1,6 @@
<div align="center">
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
[![All Contributors](https://img.shields.io/badge/all_contributors-2-orange.svg?style=flat-square)](#contributors-)
<!-- ALL-CONTRIBUTORS-BADGE:END -->
<img src="assets/header.gif"/>
<a href="#contributors-"><img src="https://img.shields.io/badge/all_contributors-2-orange.svg?style=flat-square" alt="All contributors"></a>
<img src="https://github.com/tuist/tuist/workflows/Tuist/badge.svg" alt="Tuist">
<img src="https://img.shields.io/github/v/release/tuist/tuist?include_prereleases&style=flat-square" alt="Latest Version">
<a href="http://slack.tuist.io"><img src="http://slack.tuist.io/badge.svg" alt="Slack"></a>
@ -17,6 +15,7 @@
<img src="https://img.shields.io/opencollective/all/tuistapp?style=flat-square" alt="Backers and sponsors">
<img src="https://img.shields.io/github/license/tuist/tuist?style=flat-square" alt="License">
<img src="https://img.shields.io/badge/Powered%20by-Tuist-blue" alt="Powered by Tuist">
<a href="#contributors-"><img src="https://img.shields.io/badge/all_contributors-3-orange.svg?style=flat-square" alt="Contributors"></a>
</div>
## What's Tuist 🕺
@ -55,6 +54,12 @@ The repository is a monorepo with multiple projects:
Do you want to know more about what Tuist can offer you? Or perhaps want to contribute to the project and you need a starting point? You can check out the [project documentation](https://tuist.io/docs/usage/getting-started/).
## Supported by MacStadium
MacStadium supports this project by providing Mac mini hardware that we can use for running performance tests.
<img width="200" src="assets/MacStadium.png"/>
## Contribute 👩‍💻
If you are interested in contributed to the project, our documentation has a section with resources for contributors. We recommend starting from [this page](https://tuist.io/docs/contribution/getting-started/).
@ -68,8 +73,21 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
<!-- markdownlint-disable -->
<table>
<tr>
<td align="center"><a href="https://github.com/kalkwarf"><img src="https://avatars1.githubusercontent.com/u/1033839?v=4" width="100px;" alt=""/><br /><sub><b>kalkwarf</b></sub></a><br /><a href="#ideas-kalkwarf" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center"><a href="https://github.com/kalkwarf"><img src="https://avatars1.githubusercontent.com/u/1033839?v=4" width="100px;" alt=""/><br /><sub><b>kalkwarf</b></sub></a><br /><a href="#ideas-kalkwarf" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://github.com/tuist/tuist/issues?q=author%3Akalkwarf" title="Bug reports">🐛</a></td>
<td align="center"><a href="https://github.com/fortmarek"><img src="https://avatars0.githubusercontent.com/u/9371695?v=4" width="100px;" alt=""/><br /><sub><b>Marek Fořt</b></sub></a><br /><a href="#ideas-fortmarek" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center"><a href="https://github.com/laxmorek"><img src="https://avatars2.githubusercontent.com/u/4774319?v=4" width="100px;" alt=""/><br /><sub><b>Kamil Harasimowicz</b></sub></a><br /><a href="https://github.com/tuist/tuist/commits?author=laxmorek" title="Code">💻</a> <a href="#ideas-laxmorek" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center"><a href="http://www.matrixprojects.net"><img src="https://avatars3.githubusercontent.com/u/11914919?v=4" width="100px;" alt=""/><br /><sub><b>Kas</b></sub></a><br /><a href="https://github.com/tuist/tuist/commits?author=kwridan" title="Code">💻</a></td>
<td align="center"><a href="http://natanrolnik.me"><img src="https://avatars3.githubusercontent.com/u/1164565?v=4" width="100px;" alt=""/><br /><sub><b>Natan Rolnik</b></sub></a><br /><a href="https://github.com/tuist/tuist/issues?q=author%3Anatanrolnik" title="Bug reports">🐛</a></td>
<td align="center"><a href="https://github.com/svastven"><img src="https://avatars0.githubusercontent.com/u/42235915?v=4" width="100px;" alt=""/><br /><sub><b>svastven</b></sub></a><br /><a href="#ideas-svastven" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center"><a href="http://bhuemer.github.io"><img src="https://avatars2.githubusercontent.com/u/1212480?v=4" width="100px;" alt=""/><br /><sub><b>Bernhard Huemer</b></sub></a><br /><a href="#ideas-bhuemer" title="Ideas, Planning, & Feedback">🤔</a></td>
</tr>
<tr>
<td align="center"><a href="https://djankowski.dev"><img src="https://avatars0.githubusercontent.com/u/10795657?v=4" width="100px;" alt=""/><br /><sub><b>Daniel Jankowski</b></sub></a><br /><a href="#ideas-mollyIV" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center"><a href="https://github.com/facumenzella"><img src="https://avatars1.githubusercontent.com/u/1125252?v=4" width="100px;" alt=""/><br /><sub><b>Facundo Menzella</b></sub></a><br /><a href="#ideas-facumenzella" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center"><a href="https://github.com/eito"><img src="https://avatars3.githubusercontent.com/u/775643?v=4" width="100px;" alt=""/><br /><sub><b>Eric Ito</b></sub></a><br /><a href="#ideas-eito" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center"><a href="https://github.com/olejnjak"><img src="https://avatars1.githubusercontent.com/u/3148214?v=4" width="100px;" alt=""/><br /><sub><b>Jakub Olejník</b></sub></a><br /><a href="#ideas-olejnjak" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center"><a href="https://github.com/lakpa"><img src="https://avatars0.githubusercontent.com/u/389328?v=4" width="100px;" alt=""/><br /><sub><b>ldindu</b></sub></a><br /><a href="#ideas-lakpa" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center"><a href="https://github.com/gtsifrikas"><img src="https://avatars2.githubusercontent.com/u/8904378?v=4" width="100px;" alt=""/><br /><sub><b>George Tsifrikas</b></sub></a><br /><a href="#blog-gtsifrikas" title="Blogposts">📝</a></td>
</tr>
</table>

View File

@ -2,19 +2,9 @@
This document describes the process of releasing new versions of tuist.
1. First make sure you are in master and the latest changes are pulled: `git pull origin master`
2. Ensure that the project is in a releasable state by running the tests: `swift test` and `bundle exec rake features`.
3. Determine the new version:
1. Determine the new version:
- Major if there's been a breaking change.
- Minor by default.
- Patch if it's a hotfix release.
4. Update the version in the `Constants.swift` file.
5. Update the `CHANGELOG.md` to include the version section.
6. Commit the changes and tag the commit with the version `git tag x.y.z`.
7. Select the Xcode version 11.5 before building the binaries: `sudo xcode-select -s /Applications/Xcode_11.5.app`.
8. Package and upload the release to GCS by running `bundle exec rake release`.
9. Upload the installation scripts to GCS by running `bundle exec rake release_scripts`.
10. Create a release on GitHub with the version as a title, the body from the CHANGELOG file, and attach the artifacts in the `build/` directory.
11. Run `tuist update` and verify that the new version is installed and runs.
2. Run `tapestry github-release version-number` (eg `tapestry github-release 1.1.0`) *(Install [tapestry](https://github.com/ackeecz/tapestry) and [tuist](https://github.com/tuist/tuist) if you don't have them installed already)*.

View File

@ -1,6 +1,7 @@
# frozen_string_literal: true
SWIFTDOC_VERSION = "1.0.0-beta.4".freeze
SWIFTLINT_VERSION = "0.40.2".freeze
require 'rubygems'
require 'cucumber'
@ -21,7 +22,7 @@ end
desc("Updates swift-doc binary with the latest version available.")
task :swift_doc_update do
root_dir = Dir.pwd.strip
root_dir = File.expand_path(__dir__)
Dir.mktmpdir do |temporary_dir|
Dir.chdir(temporary_dir) do
system("curl", "-LO", "https://github.com/SwiftDocOrg/swift-doc/archive/#{SWIFTDOC_VERSION}.zip")
@ -32,7 +33,20 @@ task :swift_doc_update do
system("cp", "swift-doc/swift-doc-#{SWIFTDOC_VERSION}/.build/release/swift-doc", "#{root_dir}/vendor/swift-doc")
end
end
system("rm .swift-doc.version && echo \"#{SWIFTDOC_VERSION}\" >> .swift-doc.version")
File.write(File.join(root_dir, "vendor/.swiftdoc.version"), SWIFTDOC_VERSION)
end
desc("Updates swift-lint binary with the latest version available.")
task :swift_lint_update do
root_dir = File.expand_path(__dir__)
Dir.mktmpdir do |temporary_dir|
Dir.chdir(temporary_dir) do
system("curl", "-LO", "https://github.com/realm/SwiftLint/releases/download/#{SWIFTLINT_VERSION}/portable_swiftlint.zip")
extract_zip("portable_swiftlint.zip", "portable_swiftlint")
system("cp", "portable_swiftlint/swiftlint", "#{root_dir}/vendor/swiftlint")
end
end
File.write(File.join(root_dir, "vendor/.swiftlint.version"), SWIFTLINT_VERSION)
end
desc("Formats the code style")
@ -61,6 +75,11 @@ task :style_ruby_correct do
system("bundle", "exec", "rubocop", "-a")
end
desc("Builds and archive a release version of tuist and tuistenv for local testing.")
task :local_package do
package
end
desc("Builds, archives, and publishes tuist and tuistenv for release")
task :release do
decrypt_secrets

View File

@ -8,13 +8,17 @@ public struct Config: Codable, Equatable {
///
/// - xcodeProjectName(TemplateString): When passed, Tuist generates the project with the specific name on disk instead of using the project name.
/// - organizationName(String): When passed, Tuist generates the project with the specific organization name.
/// - developmentRegion(String): When passed, Tuist generates the project with the specific development region.
/// - disableAutogeneratedSchemes: When passed, Tuist generates the project only with custom specified schemes, autogenerated default schemes are skipped
/// - disableSynthesizedResourceAccessors: When passed, Tuist does not synthesize resource accessors
/// - disableShowEnvironmentVarsInScriptPhases: When passed, Tuist disables echoing the ENV in shell script build phases
public enum GenerationOptions: Encodable, Decodable, Equatable {
case xcodeProjectName(TemplateString)
case organizationName(String)
case developmentRegion(String)
case disableAutogeneratedSchemes
case disableSynthesizedResourceAccessors
case disableShowEnvironmentVarsInScriptPhases
}
/// Generation options.
@ -26,7 +30,7 @@ public struct Config: Codable, Equatable {
/// Cloud configuration.
public let cloud: Cloud?
/// Initializes the tuist cofiguration.
/// Initializes the tuist configuration.
///
/// - Parameters:
/// - compatibleXcodeVersions: List of Xcode versions the project is compatible with.
@ -45,7 +49,12 @@ public struct Config: Codable, Equatable {
extension Config.GenerationOptions {
enum CodingKeys: String, CodingKey {
case xcodeProjectName, organizationName, disableAutogeneratedSchemes, disableSynthesizedResourceAccessors
case xcodeProjectName
case organizationName
case developmentRegion
case disableAutogeneratedSchemes
case disableSynthesizedResourceAccessors
case disableShowEnvironmentVarsInScriptPhases
}
public init(from decoder: Decoder) throws {
@ -63,6 +72,12 @@ extension Config.GenerationOptions {
self = .organizationName(organizationName)
return
}
if container.allKeys.contains(.developmentRegion), try container.decodeNil(forKey: .developmentRegion) == false {
var associatedValues = try container.nestedUnkeyedContainer(forKey: .developmentRegion)
let developmentRegion = try associatedValues.decode(String.self)
self = .developmentRegion(developmentRegion)
return
}
if container.allKeys.contains(.disableAutogeneratedSchemes), try container.decode(Bool.self, forKey: .disableAutogeneratedSchemes) {
self = .disableAutogeneratedSchemes
return
@ -71,6 +86,11 @@ extension Config.GenerationOptions {
self = .disableSynthesizedResourceAccessors
return
}
if container.allKeys.contains(.disableShowEnvironmentVarsInScriptPhases), try container.decode(Bool.self, forKey: .disableShowEnvironmentVarsInScriptPhases) {
self = .disableShowEnvironmentVarsInScriptPhases
return
}
throw DecodingError.dataCorrupted(.init(codingPath: decoder.codingPath, debugDescription: "Unknown enum case"))
}
@ -84,10 +104,15 @@ extension Config.GenerationOptions {
case let .organizationName(name):
var associatedValues = container.nestedUnkeyedContainer(forKey: .organizationName)
try associatedValues.encode(name)
case let .developmentRegion(developmentRegion):
var associatedValues = container.nestedUnkeyedContainer(forKey: .developmentRegion)
try associatedValues.encode(developmentRegion)
case .disableAutogeneratedSchemes:
try container.encode(true, forKey: .disableAutogeneratedSchemes)
case .disableSynthesizedResourceAccessors:
try container.encode(true, forKey: .disableSynthesizedResourceAccessors)
case .disableShowEnvironmentVarsInScriptPhases:
try container.encode(true, forKey: .disableShowEnvironmentVarsInScriptPhases)
}
}
}
@ -103,10 +128,14 @@ public func == (lhs: TuistConfig.GenerationOptions, rhs: TuistConfig.GenerationO
return lhs.rawString == rhs.rawString
case let (.organizationName(lhs), .organizationName(rhs)):
return lhs == rhs
case let (.developmentRegion(lhs), .developmentRegion(rhs)):
return lhs == rhs
case (.disableAutogeneratedSchemes, .disableAutogeneratedSchemes):
return true
case (.disableSynthesizedResourceAccessors, .disableSynthesizedResourceAccessors):
return true
case (.disableShowEnvironmentVarsInScriptPhases, .disableShowEnvironmentVarsInScriptPhases):
return true
default:
return false
}

View File

@ -0,0 +1,36 @@
import Foundation
/// Dependency contains the description of any kind of dependency of your Xcode project.
public struct Dependency: Codable, Equatable {
/// Name of the dependency
let name: String
/// Type of requirement for the given dependency
let requirement: Dependency.Requirement
public init(name: String, requirement: Dependency.Requirement) {
self.name = name
self.requirement = requirement
}
/// Carthage dependency initailizer
/// - Parameter name: Name of the dependency
/// - Parameter requirement: Type of requirement for the given dependency
/// - Returns Dependency
public static func carthage(name: String, requirement: Dependency.Requirement) -> Dependency {
Dependency(name: name, requirement: requirement)
}
public static func == (lhs: Dependency, rhs: Dependency) -> Bool {
lhs.name == rhs.name && lhs.requirement == rhs.requirement
}
}
public struct Dependencies: Codable, Equatable {
private let dependencies: [Dependency]
public init(_ dependencies: [Dependency]) {
self.dependencies = dependencies
dumpIfNeeded(self)
}
}

View File

@ -0,0 +1,30 @@
import Foundation
// The idea of Requirement comes from SPM PackageRequirement
// https://github.com/apple/swift-package-manager/blob/main/Sources/PackageDescription/PackageRequirement.swift
// This could be further extended for more cases
/// DependencyRequiement describes which dependency the project needs.
public extension Dependency {
enum Requirement: Codable, Equatable {
case exact(Version)
// case range(Range<Version>)
// case branch(String)
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self = .exact(try container.decode(Version.self, forKey: .exact))
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
if case let .exact(version) = self {
try container.encode(version, forKey: .exact)
}
}
enum CodingKeys: String, CodingKey {
case exact
}
}
}

View File

@ -11,6 +11,7 @@ public enum Product: String, Codable, Equatable {
case unitTests = "unit_tests"
case uiTests = "ui_tests"
case bundle
case appClip
// Not supported yet
case appExtension = "app_extension"

View File

@ -148,7 +148,7 @@ public enum DefaultSettings: Codable, Equatable {
var nestedContainer = try container.nestedUnkeyedContainer(forKey: .essential)
let excludedKeys = try nestedContainer.decode(Set<String>.self)
self = .essential(excluding: excludedKeys)
case .none:
case .none?:
self = .none
default:
throw DecodingError.dataCorrupted(

View File

@ -78,7 +78,7 @@ public extension SettingsDictionary {
merging(["VERSIONING_SYSTEM": "apple-generic"])
}
/// Sets "VERSION_INFO_PREFIX" to `version`. If `prefix` and `suffix` are not `nil`, they're used as `"VERSION_INFO_PREFIX"` and `"VERSION_INFO_SUFFIX"` respectively.
/// Sets "VERSION_INFO_STRING" to `version`. If `prefix` and `suffix` are not `nil`, they're used as `"VERSION_INFO_PREFIX"` and `"VERSION_INFO_SUFFIX"` respectively.
func versionInfo(_ version: String, prefix: String? = nil, suffix: String? = nil) -> SettingsDictionary {
var versionSettings: SettingsDictionary = ["VERSION_INFO_STRING": SettingValue(version)]
versionSettings["VERSION_INFO_PREFIX"] = prefix.map { SettingValue($0) }

View File

@ -37,6 +37,9 @@ public struct TargetAction: Codable, Equatable {
/// List of output filelist paths
public let outputFileListPaths: [Path]
/// Whether to skip running this script in incremental builds, if nothing has changed
public let basedOnDependencyAnalysis: Bool?
public enum CodingKeys: String, CodingKey {
case name
case tool
@ -47,6 +50,7 @@ public struct TargetAction: Codable, Equatable {
case inputFileListPaths
case outputPaths
case outputFileListPaths
case basedOnDependencyAnalysis
}
/// Initializes the target action with its attributes.
@ -61,6 +65,7 @@ public struct TargetAction: Codable, Equatable {
/// - inputFileListPaths: List of input filelist paths.
/// - outputPaths: List of output file paths.
/// - outputFileListPaths: List of output filelist paths.
/// - basedOnDependencyAnalysis: Whether to skip running this script in incremental builds
init(name: String,
tool: String?,
path: Path?,
@ -69,7 +74,8 @@ public struct TargetAction: Codable, Equatable {
inputPaths: [Path] = [],
inputFileListPaths: [Path] = [],
outputPaths: [Path] = [],
outputFileListPaths: [Path] = [])
outputFileListPaths: [Path] = [],
basedOnDependencyAnalysis: Bool? = nil)
{
self.name = name
self.path = path
@ -80,6 +86,7 @@ public struct TargetAction: Codable, Equatable {
self.inputFileListPaths = inputFileListPaths
self.outputPaths = outputPaths
self.outputFileListPaths = outputFileListPaths
self.basedOnDependencyAnalysis = basedOnDependencyAnalysis
}
/// Returns a target action that gets executed before the sources and resources build phase.
@ -92,6 +99,7 @@ public struct TargetAction: Codable, Equatable {
/// - inputFileListPaths: List of input filelist paths.
/// - outputPaths: List of output file paths.
/// - outputFileListPaths: List of output filelist paths.
/// - basedOnDependencyAnalysis: Whether to skip running this script in incremental builds
/// - Returns: Target action.
public static func pre(tool: String,
arguments: String...,
@ -99,7 +107,8 @@ public struct TargetAction: Codable, Equatable {
inputPaths: [Path] = [],
inputFileListPaths: [Path] = [],
outputPaths: [Path] = [],
outputFileListPaths: [Path] = []) -> TargetAction
outputFileListPaths: [Path] = [],
basedOnDependencyAnalysis: Bool? = nil) -> TargetAction
{
TargetAction(name: name,
tool: tool,
@ -109,7 +118,8 @@ public struct TargetAction: Codable, Equatable {
inputPaths: inputPaths,
inputFileListPaths: inputFileListPaths,
outputPaths: outputPaths,
outputFileListPaths: outputFileListPaths)
outputFileListPaths: outputFileListPaths,
basedOnDependencyAnalysis: basedOnDependencyAnalysis)
}
/// Returns a target action that gets executed before the sources and resources build phase.
@ -122,6 +132,7 @@ public struct TargetAction: Codable, Equatable {
/// - inputFileListPaths: List of input filelist paths.
/// - outputPaths: List of output file paths.
/// - outputFileListPaths: List of output filelist paths.
/// - basedOnDependencyAnalysis: Whether to skip running this script in incremental builds
/// - Returns: Target action.
public static func pre(tool: String,
arguments: [String],
@ -129,7 +140,8 @@ public struct TargetAction: Codable, Equatable {
inputPaths: [Path] = [],
inputFileListPaths: [Path] = [],
outputPaths: [Path] = [],
outputFileListPaths: [Path] = []) -> TargetAction
outputFileListPaths: [Path] = [],
basedOnDependencyAnalysis: Bool? = nil) -> TargetAction
{
TargetAction(name: name,
tool: tool,
@ -139,7 +151,8 @@ public struct TargetAction: Codable, Equatable {
inputPaths: inputPaths,
inputFileListPaths: inputFileListPaths,
outputPaths: outputPaths,
outputFileListPaths: outputFileListPaths)
outputFileListPaths: outputFileListPaths,
basedOnDependencyAnalysis: basedOnDependencyAnalysis)
}
/// Returns a target action that gets executed before the sources and resources build phase.
@ -152,6 +165,7 @@ public struct TargetAction: Codable, Equatable {
/// - inputFileListPaths: List of input filelist paths.
/// - outputPaths: List of output file paths.
/// - outputFileListPaths: List of output filelist paths.
/// - basedOnDependencyAnalysis: Whether to skip running this script in incremental builds
/// - Returns: Target action.
public static func pre(path: Path,
arguments: String...,
@ -159,7 +173,8 @@ public struct TargetAction: Codable, Equatable {
inputPaths: [Path] = [],
inputFileListPaths: [Path] = [],
outputPaths: [Path] = [],
outputFileListPaths: [Path] = []) -> TargetAction
outputFileListPaths: [Path] = [],
basedOnDependencyAnalysis: Bool? = nil) -> TargetAction
{
TargetAction(name: name,
tool: nil,
@ -169,7 +184,8 @@ public struct TargetAction: Codable, Equatable {
inputPaths: inputPaths,
inputFileListPaths: inputFileListPaths,
outputPaths: outputPaths,
outputFileListPaths: outputFileListPaths)
outputFileListPaths: outputFileListPaths,
basedOnDependencyAnalysis: basedOnDependencyAnalysis)
}
/// Returns a target action that gets executed before the sources and resources build phase.
@ -182,6 +198,7 @@ public struct TargetAction: Codable, Equatable {
/// - inputFileListPaths: List of input filelist paths.
/// - outputPaths: List of output file paths.
/// - outputFileListPaths: List of output filelist paths.
/// - basedOnDependencyAnalysis: Whether to skip running this script in incremental builds
/// - Returns: Target action.
public static func pre(path: Path,
arguments: [String],
@ -189,7 +206,8 @@ public struct TargetAction: Codable, Equatable {
inputPaths: [Path] = [],
inputFileListPaths: [Path] = [],
outputPaths: [Path] = [],
outputFileListPaths: [Path] = []) -> TargetAction
outputFileListPaths: [Path] = [],
basedOnDependencyAnalysis: Bool? = nil) -> TargetAction
{
TargetAction(name: name,
tool: nil,
@ -199,7 +217,8 @@ public struct TargetAction: Codable, Equatable {
inputPaths: inputPaths,
inputFileListPaths: inputFileListPaths,
outputPaths: outputPaths,
outputFileListPaths: outputFileListPaths)
outputFileListPaths: outputFileListPaths,
basedOnDependencyAnalysis: basedOnDependencyAnalysis)
}
/// Returns a target action that gets executed after the sources and resources build phase.
@ -212,6 +231,7 @@ public struct TargetAction: Codable, Equatable {
/// - inputFileListPaths: List of input filelist paths.
/// - outputPaths: List of output file paths.
/// - outputFileListPaths: List of output filelist paths.
/// - basedOnDependencyAnalysis: Whether to skip running this script in incremental builds
/// - Returns: Target action.
public static func post(tool: String,
arguments: String...,
@ -219,7 +239,8 @@ public struct TargetAction: Codable, Equatable {
inputPaths: [Path] = [],
inputFileListPaths: [Path] = [],
outputPaths: [Path] = [],
outputFileListPaths: [Path] = []) -> TargetAction
outputFileListPaths: [Path] = [],
basedOnDependencyAnalysis: Bool? = nil) -> TargetAction
{
TargetAction(name: name,
tool: tool,
@ -229,7 +250,8 @@ public struct TargetAction: Codable, Equatable {
inputPaths: inputPaths,
inputFileListPaths: inputFileListPaths,
outputPaths: outputPaths,
outputFileListPaths: outputFileListPaths)
outputFileListPaths: outputFileListPaths,
basedOnDependencyAnalysis: basedOnDependencyAnalysis)
}
/// Returns a target action that gets executed after the sources and resources build phase.
@ -242,6 +264,7 @@ public struct TargetAction: Codable, Equatable {
/// - inputFileListPaths: List of input filelist paths.
/// - outputPaths: List of output file paths.
/// - outputFileListPaths: List of output filelist paths.
/// - basedOnDependencyAnalysis: Whether to skip running this script in incremental builds
/// - Returns: Target action.
public static func post(tool: String,
arguments: [String],
@ -249,7 +272,8 @@ public struct TargetAction: Codable, Equatable {
inputPaths: [Path] = [],
inputFileListPaths: [Path] = [],
outputPaths: [Path] = [],
outputFileListPaths: [Path] = []) -> TargetAction
outputFileListPaths: [Path] = [],
basedOnDependencyAnalysis: Bool? = nil) -> TargetAction
{
TargetAction(name: name,
tool: tool,
@ -259,7 +283,8 @@ public struct TargetAction: Codable, Equatable {
inputPaths: inputPaths,
inputFileListPaths: inputFileListPaths,
outputPaths: outputPaths,
outputFileListPaths: outputFileListPaths)
outputFileListPaths: outputFileListPaths,
basedOnDependencyAnalysis: basedOnDependencyAnalysis)
}
/// Returns a target action that gets executed after the sources and resources build phase.
@ -272,6 +297,7 @@ public struct TargetAction: Codable, Equatable {
/// - inputFileListPaths: List of input filelist paths.
/// - outputPaths: List of output file paths.
/// - outputFileListPaths: List of output filelist paths.
/// - basedOnDependencyAnalysis: Whether to skip running this script in incremental builds
/// - Returns: Target action.
public static func post(path: Path,
arguments: String...,
@ -279,7 +305,8 @@ public struct TargetAction: Codable, Equatable {
inputPaths: [Path] = [],
inputFileListPaths: [Path] = [],
outputPaths: [Path] = [],
outputFileListPaths: [Path] = []) -> TargetAction
outputFileListPaths: [Path] = [],
basedOnDependencyAnalysis: Bool? = nil) -> TargetAction
{
TargetAction(name: name,
tool: nil,
@ -289,7 +316,8 @@ public struct TargetAction: Codable, Equatable {
inputPaths: inputPaths,
inputFileListPaths: inputFileListPaths,
outputPaths: outputPaths,
outputFileListPaths: outputFileListPaths)
outputFileListPaths: outputFileListPaths,
basedOnDependencyAnalysis: basedOnDependencyAnalysis)
}
/// Returns a target action that gets executed after the sources and resources build phase.
@ -302,6 +330,7 @@ public struct TargetAction: Codable, Equatable {
/// - inputFileListPaths: List of input filelist paths.
/// - outputPaths: List of output file paths.
/// - outputFileListPaths: List of output filelist paths.
/// - basedOnDependencyAnalysis: Whether to skip running this script in incremental builds
/// - Returns: Target action.
public static func post(path: Path,
arguments: [String],
@ -309,7 +338,8 @@ public struct TargetAction: Codable, Equatable {
inputPaths: [Path] = [],
inputFileListPaths: [Path] = [],
outputPaths: [Path] = [],
outputFileListPaths: [Path] = []) -> TargetAction
outputFileListPaths: [Path] = [],
basedOnDependencyAnalysis: Bool? = nil) -> TargetAction
{
TargetAction(name: name,
tool: nil,
@ -319,7 +349,8 @@ public struct TargetAction: Codable, Equatable {
inputPaths: inputPaths,
inputFileListPaths: inputFileListPaths,
outputPaths: outputPaths,
outputFileListPaths: outputFileListPaths)
outputFileListPaths: outputFileListPaths,
basedOnDependencyAnalysis: basedOnDependencyAnalysis)
}
// MARK: - Codable
@ -333,6 +364,7 @@ public struct TargetAction: Codable, Equatable {
inputFileListPaths = try container.decodeIfPresent([Path].self, forKey: .inputFileListPaths) ?? []
outputPaths = try container.decodeIfPresent([Path].self, forKey: .outputPaths) ?? []
outputFileListPaths = try container.decodeIfPresent([Path].self, forKey: .outputFileListPaths) ?? []
basedOnDependencyAnalysis = try container.decodeIfPresent(Bool.self, forKey: .basedOnDependencyAnalysis)
if let path = try container.decodeIfPresent(Path.self, forKey: .path) {
self.path = path
tool = nil
@ -352,6 +384,7 @@ public struct TargetAction: Codable, Equatable {
try container.encode(inputFileListPaths, forKey: .inputFileListPaths)
try container.encode(outputPaths, forKey: .outputPaths)
try container.encode(outputFileListPaths, forKey: .outputFileListPaths)
try container.encode(basedOnDependencyAnalysis, forKey: .basedOnDependencyAnalysis)
if let tool = tool {
try container.encode(tool, forKey: .tool)

View File

@ -1,9 +0,0 @@
import Foundation
struct SimulatorDeviceAndRuntime: Hashable {
/// Device
let device: SimulatorDevice
/// Device's runtime.
let runtime: SimulatorRuntime
}

View File

@ -19,6 +19,12 @@ public protocol BuildGraphInspecting {
/// - graph: Dependency graph.
func buildableTarget(scheme: Scheme, graph: Graph) -> Target?
/// From the list of testable targets of the given scheme, it returns the first one.
/// - Parameters:
/// - scheme: Scheme in which to look up the target.
/// - graph: Dependency graph.
func testableTarget(scheme: Scheme, graph: Graph) -> Target?
/// Given a graph, it returns a list of buildable schemes.
/// - Parameter graph: Dependency graph.
func buildableSchemes(graph: Graph) -> [Scheme]
@ -27,6 +33,14 @@ public protocol BuildGraphInspecting {
/// - Parameters:
/// - graph: Dependency graph
func buildableEntrySchemes(graph: Graph) -> [Scheme]
/// Given a graph, it returns a list of test schemes (those that include only one test target).
/// - Parameter graph: Dependency graph.
func testSchemes(graph: Graph) -> [Scheme]
/// Given a graph, it returns a list of testable schemes.
/// - Parameter graph: Dependency graph.
func testableSchemes(graph: Graph) -> [Scheme]
}
public class BuildGraphInspector: BuildGraphInspecting {
@ -53,17 +67,26 @@ public class BuildGraphInspector: BuildGraphInspecting {
}
public func buildableTarget(scheme: Scheme, graph: Graph) -> Target? {
if scheme.buildAction?.targets.count == 0 {
guard
scheme.buildAction?.targets.isEmpty == false,
let buildTarget = scheme.buildAction?.targets.first
else {
return nil
}
let buildTarget = scheme.buildAction!.targets.first!
return graph.target(path: buildTarget.projectPath, name: buildTarget.name)!.target
return graph.target(path: buildTarget.projectPath, name: buildTarget.name)?.target
}
public func testableTarget(scheme: Scheme, graph: Graph) -> Target? {
if scheme.testAction?.targets.count == 0 {
return nil
}
let testTarget = scheme.testAction!.targets.first!
return graph.target(path: testTarget.target.projectPath, name: testTarget.target.name)!.target
}
public func buildableSchemes(graph: Graph) -> [Scheme] {
graph.targets.values.flatMap {
$0.flatMap { $0.project.schemes }
}
graph.schemes
.filter { $0.buildAction?.targets.isEmpty == false }
.sorted(by: { $0.name < $1.name })
}
@ -76,6 +99,25 @@ public class BuildGraphInspector: BuildGraphInspecting {
.sorted(by: { $0.name < $1.name })
}
public func testableSchemes(graph: Graph) -> [Scheme] {
graph.schemes
.filter { $0.testAction?.targets.isEmpty == false }
.sorted(by: { $0.name < $1.name })
}
public func testSchemes(graph: Graph) -> [Scheme] {
graph.targets.values.flatMap { target -> [Scheme] in
target
.filter { $0.target.product == .unitTests || $0.target.product == .uiTests }
.flatMap { target -> [Scheme] in
target.project.schemes
.filter { $0.targetDependencies().map(\.name) == [target.name] }
}
}
.filter { $0.testAction?.targets.isEmpty == false }
.sorted(by: { $0.name < $1.name })
}
public func workspacePath(directory: AbsolutePath) throws -> AbsolutePath? {
try directory.glob("**/*.xcworkspace")
.filter {

View File

@ -53,6 +53,40 @@ public final class XcodeBuildController: XcodeBuildControlling {
return run(command: command)
}
public func test(
_ target: XcodeBuildTarget,
scheme: String,
clean: Bool = false,
destination: XcodeBuildDestination,
arguments: [XcodeBuildArgument]
) -> Observable<SystemEvent<XcodeBuildOutput>> {
var command = ["/usr/bin/xcrun", "xcodebuild"]
// Action
if clean {
command.append("clean")
}
command.append("test")
// Scheme
command.append(contentsOf: ["-scheme", scheme])
// Target
command.append(contentsOf: target.xcodebuildArguments)
// Arguments
command.append(contentsOf: arguments.flatMap { $0.arguments })
switch destination {
case let .device(udid):
command.append(contentsOf: ["-destination", "id=\(udid)"])
case .mac:
break
}
return run(command: command)
}
public func archive(_ target: XcodeBuildTarget,
scheme: String,
clean: Bool,
@ -168,29 +202,18 @@ public final class XcodeBuildController: XcodeBuildControlling {
switch event {
case let .standardError(errorData):
guard let line = String(data: errorData, encoding: .utf8) else { return Observable.empty() }
return Observable.create { observer in
let lines = line.split(separator: "\n")
lines.map { line in
let output = line.split(separator: "\n").map { line -> SystemEvent<XcodeBuildOutput> in
let formatedOutput = self.parser.parse(line: String(line), colored: colored)
return SystemEvent.standardError(XcodeBuildOutput(raw: "\(String(line))\n", formatted: formatedOutput.map { "\($0)\n" }))
}
.forEach(observer.onNext)
observer.onCompleted()
return Disposables.create()
}
return Observable.from(output)
case let .standardOutput(outputData):
guard let line = String(data: outputData, encoding: .utf8) else { return Observable.empty() }
return Observable.create { observer in
let lines = line.split(separator: "\n")
lines.map { line in
let output = line.split(separator: "\n").map { line -> SystemEvent<XcodeBuildOutput> in
let formatedOutput = self.parser.parse(line: String(line), colored: colored)
return SystemEvent.standardOutput(XcodeBuildOutput(raw: "\(String(line))\n", formatted: formatedOutput.map { "\($0)\n" }))
}
.forEach(observer.onNext)
observer.onCompleted()
return Disposables.create()
}
return Observable.from(output)
}
}
}

View File

@ -43,4 +43,27 @@ public final class MockBuildGraphInspector: BuildGraphInspecting {
return []
}
}
public var testableTargetStub: ((Scheme, Graph) -> Target?)?
public func testableTarget(scheme: Scheme, graph: Graph) -> Target? {
if let testableTargetStub = testableTargetStub {
return testableTargetStub(scheme, graph)
} else {
return Target.test()
}
}
public var testableSchemesStub: ((Graph) -> [Scheme])?
public func testableSchemes(graph: Graph) -> [Scheme] {
if let testableSchemesStub = testableSchemesStub {
return testableSchemesStub(graph)
} else {
return []
}
}
public var testSchemesStub: ((Graph) -> [Scheme])?
public func testSchemes(graph: Graph) -> [Scheme] {
testSchemesStub?(graph) ?? []
}
}

View File

@ -48,8 +48,8 @@ public final class Cache: CacheStoring {
}!
}
public func store(hash: String, xcframeworkPath: AbsolutePath) -> Completable {
public func store(hash: String, paths: [AbsolutePath]) -> Completable {
let storages = storageProvider.storages()
return Completable.zip(storages.map { $0.store(hash: hash, xcframeworkPath: xcframeworkPath) })
return Completable.zip(storages.map { $0.store(hash: hash, paths: paths) })
}
}

View File

@ -29,7 +29,7 @@ public final class CacheLocalStorage: CacheStoring {
// MARK: - Init
public convenience init() {
self.init(cacheDirectory: Environment.shared.xcframeworksCacheDirectory)
self.init(cacheDirectory: Environment.shared.buildCacheDirectory)
}
init(cacheDirectory: AbsolutePath) {
@ -40,14 +40,14 @@ public final class CacheLocalStorage: CacheStoring {
public func exists(hash: String) -> Single<Bool> {
Single.create { (completed) -> Disposable in
completed(.success(FileHandler.shared.glob(self.cacheDirectory, glob: "\(hash)/*").count != 0))
completed(.success(self.lookupFramework(directory: self.cacheDirectory.appending(component: hash)) != nil))
return Disposables.create()
}
}
public func fetch(hash: String) -> Single<AbsolutePath> {
Single.create { (completed) -> Disposable in
if let path = FileHandler.shared.glob(self.cacheDirectory, glob: "\(hash)/*").first {
if let path = self.lookupFramework(directory: self.cacheDirectory.appending(component: hash)) {
completed(.success(path))
} else {
completed(.error(CacheLocalStorageError.xcframeworkNotFound(hash: hash)))
@ -56,21 +56,22 @@ public final class CacheLocalStorage: CacheStoring {
}
}
public func store(hash: String, xcframeworkPath: AbsolutePath) -> Completable {
public func store(hash: String, paths: [AbsolutePath]) -> Completable {
let copy = Completable.create { (completed) -> Disposable in
let hashFolder = self.cacheDirectory.appending(component: hash)
let destinationPath = hashFolder.appending(component: xcframeworkPath.basename)
do {
if !FileHandler.shared.exists(hashFolder) {
try FileHandler.shared.createFolder(hashFolder)
}
try paths.forEach { sourcePath in
let destinationPath = hashFolder.appending(component: sourcePath.basename)
if FileHandler.shared.exists(destinationPath) {
try FileHandler.shared.delete(destinationPath)
}
try FileHandler.shared.copy(from: xcframeworkPath, to: destinationPath)
try FileHandler.shared.copy(from: sourcePath, to: destinationPath)
}
} catch {
completed(.error(error))
return Disposables.create()
@ -84,6 +85,14 @@ public final class CacheLocalStorage: CacheStoring {
// MARK: - Fileprivate
fileprivate func lookupFramework(directory: AbsolutePath) -> AbsolutePath? {
let extensions = ["framework", "xcframework"]
for ext in extensions {
if let filePath = FileHandler.shared.glob(directory, glob: "*.\(ext)").first { return filePath }
}
return nil
}
fileprivate func createCacheDirectory() -> Completable {
Completable.create { (completed) -> Disposable in
do {

View File

@ -5,18 +5,18 @@ import TuistCore
import TuistSupport
enum CacheRemoteStorageError: FatalError, Equatable {
case archiveDoesNotContainXCFramework(AbsolutePath)
case frameworkNotFound(hash: String)
var type: ErrorType {
switch self {
case .archiveDoesNotContainXCFramework: return .abort
case .frameworkNotFound: return .abort
}
}
var description: String {
switch self {
case let .archiveDoesNotContainXCFramework(path):
return "Unzipped archive at path \(path.pathString) does not contain any xcframework."
case let .frameworkNotFound(hash):
return "The downloaded artifact with hash '\(hash)' has an incorrect format and doesn't contain a xcframework nor a framework."
}
}
}
@ -28,21 +28,20 @@ public final class CacheRemoteStorage: CacheStoring {
private let cloudConfig: Cloud
private let cloudClient: CloudClienting
private let fileClient: FileClienting
private let fileArchiverFactory: FileArchiverManufacturing
private var fileArchiverMap: [AbsolutePath: FileArchiving] = [:]
private let fileArchiverFactory: FileArchivingFactorying
// MARK: - Init
public convenience init(cloudConfig: Cloud, cloudClient: CloudClienting) {
self.init(cloudConfig: cloudConfig,
cloudClient: cloudClient,
fileArchiverFactory: FileArchiverFactory(),
fileArchiverFactory: FileArchivingFactory(),
fileClient: FileClient())
}
init(cloudConfig: Cloud,
cloudClient: CloudClienting,
fileArchiverFactory: FileArchiverManufacturing,
fileArchiverFactory: FileArchivingFactorying,
fileClient: FileClienting)
{
self.cloudConfig = cloudConfig
@ -79,7 +78,10 @@ public final class CacheRemoteStorage: CacheStoring {
return cloudClient
.request(resource)
.map { $0.object.data.url }
.flatMap { (url: URL) in self.fileClient.download(url: url) }
.flatMap { (url: URL) in
self.fileClient.download(url: url)
.do(onSubscribed: { logger.info("Downloading cache artifact with hash \(hash).") })
}
.flatMap { (filePath: AbsolutePath) in
do {
let archiveContentPath = try self.unzip(downloadedArchive: filePath, hash: hash)
@ -93,10 +95,10 @@ public final class CacheRemoteStorage: CacheStoring {
}
}
public func store(hash: String, xcframeworkPath: AbsolutePath) -> Completable {
public func store(hash: String, paths: [AbsolutePath]) -> Completable {
do {
let archiver = fileArchiver(for: xcframeworkPath)
let destinationZipPath = try archiver.zip()
let archiver = try fileArchiverFactory.makeFileArchiver(for: paths)
let destinationZipPath = try archiver.zip(name: hash)
let resource = try CloudCacheResponse.storeResource(
hash: hash,
cloud: cloudConfig,
@ -119,26 +121,31 @@ public final class CacheRemoteStorage: CacheStoring {
// MARK: - Private
private func xcframeworkPath(in archive: AbsolutePath) throws -> AbsolutePath? {
let folderContent = try FileHandler.shared.contentsOfDirectory(archive)
return folderContent.filter { FileHandler.shared.isFolder($0) && $0.extension == "xcframework" }.first
private func frameworkPath(in archive: AbsolutePath) -> AbsolutePath? {
if let xcframeworkPath = FileHandler.shared.glob(archive, glob: "*.xcframework").first {
return xcframeworkPath
} else if let frameworkPath = FileHandler.shared.glob(archive, glob: "*.framework").first {
return frameworkPath
}
return nil
}
private func unzip(downloadedArchive: AbsolutePath, hash: String) throws -> AbsolutePath {
let zipPath = try FileHandler.shared.changeExtension(path: downloadedArchive, to: "zip")
let archiveDestination = Environment.shared.xcframeworksCacheDirectory.appending(component: hash)
try fileArchiver(for: zipPath).unzip(to: archiveDestination)
guard let xcframework = try xcframeworkPath(in: archiveDestination) else {
try FileHandler.shared.delete(archiveDestination)
throw CacheRemoteStorageError.archiveDoesNotContainXCFramework(archiveDestination)
let archiveDestination = Environment.shared.buildCacheDirectory.appending(component: hash)
let fileUnarchiver = try fileArchiverFactory.makeFileUnarchiver(for: zipPath)
let unarchivedDirectory = try fileUnarchiver.unzip()
defer {
try? fileUnarchiver.delete()
}
return xcframework
if frameworkPath(in: unarchivedDirectory) == nil {
throw CacheRemoteStorageError.frameworkNotFound(hash: hash)
}
private func fileArchiver(for path: AbsolutePath) -> FileArchiving {
let fileArchiver = fileArchiverMap[path] ?? fileArchiverFactory.makeFileArchiver(for: path)
fileArchiverMap[path] = fileArchiver
return fileArchiver
if !FileHandler.shared.exists(archiveDestination.parentDirectory) {
try FileHandler.shared.createFolder(archiveDestination.parentDirectory)
}
try FileHandler.shared.move(from: unarchivedDirectory, to: archiveDestination)
return frameworkPath(in: archiveDestination)!
}
private func deleteZipArchiveCompletable(archiver: FileArchiving) -> Completable {
@ -152,12 +159,4 @@ public final class CacheRemoteStorage: CacheStoring {
return Disposables.create {}
})
}
// MARK: - Deinit
deinit {
do {
try fileArchiverMap.values.forEach { fileArchiver in try fileArchiver.delete() }
} catch {}
}
}

View File

@ -21,6 +21,6 @@ public protocol CacheStoring {
/// It stores the xcframework at the given path in the cache.
/// - Parameters:
/// - hash: Hash of the target the xcframework belongs to.
/// - xcframeworkPath: Path to the .xcframework.
func store(hash: String, xcframeworkPath: AbsolutePath) -> Completable
/// - paths: Path to the files that will be stored.
func store(hash: String, paths: [AbsolutePath]) -> Completable
}

View File

@ -16,7 +16,7 @@ public final class DependenciesContentHasher: DependenciesContentHashing {
self.contentHasher = contentHasher
}
// MARK: - HeadersContentHashing
// MARK: - DependenciesContentHashing
public func hash(dependencies: [Dependency]) throws -> String {
let hashes = dependencies.map { try? hash(dependency: $0) }

View File

@ -5,7 +5,7 @@ import TuistCore
import TuistSupport
public protocol GraphContentHashing {
func contentHashes(for graph: TuistCore.Graph) throws -> [TargetNode: String]
func contentHashes(for graph: TuistCore.Graph, cacheOutputType: CacheOutputType) throws -> [TargetNode: String]
}
/// `GraphContentHasher`
@ -24,7 +24,7 @@ public final class GraphContentHasher: GraphContentHashing {
// MARK: - GraphContentHashing
public func contentHashes(for graph: TuistCore.Graph) throws -> [TargetNode: String] {
public func contentHashes(for graph: TuistCore.Graph, cacheOutputType: CacheOutputType) throws -> [TargetNode: String] {
var visitedNodes: [TargetNode: Bool] = [:]
let hashableTargets = graph.targets.values.flatMap { (targets: [TargetNode]) -> [TargetNode] in
targets.compactMap { target in
@ -35,7 +35,8 @@ public final class GraphContentHasher: GraphContentHashing {
}
}
let hashes = try hashableTargets.map {
try targetContentHasher.contentHash(for: $0)
try targetContentHasher.contentHash(for: $0,
cacheOutputType: cacheOutputType)
}
return Dictionary(uniqueKeysWithValues: zip(hashableTargets, hashes))
}

View File

@ -16,6 +16,8 @@ public final class ResourcesContentHasher: ResourcesContentHashing {
self.contentHasher = contentHasher
}
// MARK: - ResourcesContentHashing
public func hash(resources: [FileElement]) throws -> String {
let hashes = try resources.map { try contentHasher.hash(path: $0.path) }
return try contentHasher.hash(hashes)

View File

@ -16,10 +16,10 @@ public final class SettingsContentHasher: SettingsContentHashing {
self.contentHasher = contentHasher
}
// MARK: - InfoPlistContentHashing
// MARK: - SettingsContentHashing
public func hash(settings: Settings) throws -> String {
let baseSettingsHash = hash(settings.base)
let baseSettingsHash = try hash(settings.base)
let configurationHash = try hash(settings.configurations)
let defaultSettingsHash = try hash(settings.defaultSettings)
return try contentHasher.hash([baseSettingsHash, configurationHash, defaultSettingsHash])
@ -39,12 +39,15 @@ public final class SettingsContentHasher: SettingsContentHashing {
return try contentHasher.hash(configurationHashes)
}
private func hash(_ settingsDictionary: SettingsDictionary) -> String {
settingsDictionary.map { "\($0):\($1.normalize())" }.joined(separator: "-")
private func hash(_ settingsDictionary: SettingsDictionary) throws -> String {
let sortedAndNormalizedSettings = settingsDictionary
.sorted(by: { $0.0 < $1.0 })
.map { "\($0):\($1.normalize())" }.joined(separator: "-")
return try contentHasher.hash(sortedAndNormalizedSettings)
}
private func hash(_ configuration: Configuration) throws -> String {
var configurationHash = hash(configuration.settings)
var configurationHash = try hash(configuration.settings)
if let xcconfigPath = configuration.xcconfig {
let xcconfigHash = try contentHasher.hash(path: xcconfigPath)
configurationHash += xcconfigHash

View File

@ -4,7 +4,7 @@ import TuistCore
import TuistSupport
public protocol TargetContentHashing {
func contentHash(for target: TargetNode) throws -> String
func contentHash(for target: TargetNode, cacheOutputType: CacheOutputType) throws -> String
}
/// `TargetContentHasher`
@ -14,6 +14,7 @@ public final class TargetContentHasher: TargetContentHashing {
private let coreDataModelsContentHasher: CoreDataModelsContentHashing
private let sourceFilesContentHasher: SourceFilesContentHashing
private let targetActionsContentHasher: TargetActionsContentHashing
private let targetScriptsContentHasher: TargetScriptsContentHashing
private let resourcesContentHasher: ResourcesContentHashing
private let headersContentHasher: HeadersContentHashing
private let deploymentTargetContentHasher: DeploymentTargetContentHashing
@ -28,6 +29,7 @@ public final class TargetContentHasher: TargetContentHashing {
contentHasher: contentHasher,
sourceFilesContentHasher: SourceFilesContentHasher(contentHasher: contentHasher),
targetActionsContentHasher: TargetActionsContentHasher(contentHasher: contentHasher),
targetScriptsContentHasher: TargetScriptsContentHasher(contentHasher: contentHasher),
coreDataModelsContentHasher: CoreDataModelsContentHasher(contentHasher: contentHasher),
resourcesContentHasher: ResourcesContentHasher(contentHasher: contentHasher),
headersContentHasher: HeadersContentHasher(contentHasher: contentHasher),
@ -42,6 +44,7 @@ public final class TargetContentHasher: TargetContentHashing {
contentHasher: ContentHashing,
sourceFilesContentHasher: SourceFilesContentHashing,
targetActionsContentHasher: TargetActionsContentHashing,
targetScriptsContentHasher: TargetScriptsContentHashing,
coreDataModelsContentHasher: CoreDataModelsContentHashing,
resourcesContentHasher: ResourcesContentHashing,
headersContentHasher: HeadersContentHashing,
@ -54,6 +57,7 @@ public final class TargetContentHasher: TargetContentHashing {
self.sourceFilesContentHasher = sourceFilesContentHasher
self.coreDataModelsContentHasher = coreDataModelsContentHasher
self.targetActionsContentHasher = targetActionsContentHasher
self.targetScriptsContentHasher = targetScriptsContentHasher
self.resourcesContentHasher = resourcesContentHasher
self.headersContentHasher = headersContentHasher
self.deploymentTargetContentHasher = deploymentTargetContentHasher
@ -64,12 +68,13 @@ public final class TargetContentHasher: TargetContentHashing {
// MARK: - TargetContentHashing
public func contentHash(for targetNode: TargetNode) throws -> String {
public func contentHash(for targetNode: TargetNode, cacheOutputType: CacheOutputType) throws -> String {
let target = targetNode.target
let sourcesHash = try sourceFilesContentHasher.hash(sources: target.sources)
let resourcesHash = try resourcesContentHasher.hash(resources: target.resources)
let coreDataModelHash = try coreDataModelsContentHasher.hash(coreDataModels: target.coreDataModels)
let targetActionsHash = try targetActionsContentHasher.hash(targetActions: target.actions)
let targetScriptsHash = try targetScriptsContentHasher.hash(targetScripts: target.scripts)
let dependenciesHash = try dependenciesContentHasher.hash(dependencies: target.dependencies)
let environmentHash = try contentHasher.hash(target.environment)
var stringsToHash = [target.name,
@ -82,6 +87,7 @@ public final class TargetContentHasher: TargetContentHashing {
resourcesHash,
coreDataModelHash,
targetActionsHash,
targetScriptsHash,
environmentHash]
if let headers = target.headers {
let headersHash = try headersContentHasher.hash(headers: headers)
@ -104,6 +110,7 @@ public final class TargetContentHasher: TargetContentHashing {
stringsToHash.append(settingsHash)
}
stringsToHash.append(cacheOutputType.description)
return try contentHasher.hash(stringsToHash)
}
}

View File

@ -0,0 +1,35 @@
import Foundation
import TSCBasic
import TuistCore
public protocol TargetScriptsContentHashing {
func hash(targetScripts: [TargetScript]) throws -> String
}
/// `TargetScriptsContentHasher`
/// is responsible for computing a unique hash that identifies a list of target scripts
public final class TargetScriptsContentHasher: TargetScriptsContentHashing {
private let contentHasher: ContentHashing
// MARK: - Init
public init(contentHasher: ContentHashing) {
self.contentHasher = contentHasher
}
// MARK: - TargetScriptsContentHashing
/// Returns the hash that uniquely identifies an array of target actions
/// The hash takes into consideration the content of the script to execute, the content of input/output files, the name of the tool to execute, the order, the arguments and its name
public func hash(targetScripts: [TargetScript]) throws -> String {
var stringsToHash: [String] = []
for targetScript in targetScripts {
if targetScript.hashable {
stringsToHash.append(targetScript.name)
stringsToHash.append(targetScript.script)
stringsToHash.append("\(targetScript.showEnvVarsInLog)")
}
}
return try contentHasher.hash(stringsToHash)
}
}

View File

@ -0,0 +1,32 @@
import Foundation
import RxBlocking
import RxSwift
import TSCBasic
import TuistCore
import TuistSupport
public class CacheBuildPhaseProjectMapper: ProjectMapping {
public init() {}
public func map(project: Project) throws -> (Project, [SideEffectDescriptor]) {
let project = project.with(targets: project.targets.map { target in
var target = target
if target.product.isFramework {
target.scripts.append(.init(name: "[Tuist] Create file to locate the built products directory",
script: script(target: target),
showEnvVarsInLog: true,
hashable: false))
}
return target
})
return (project, [])
}
fileprivate func script(target: Target) -> String {
"""
if [ -n "$\(target.targetLocatorBuildPhaseVariable)" ]; then
touch $BUILT_PRODUCTS_DIR/.$\(target.targetLocatorBuildPhaseVariable).tuist
fi
"""
}
}

View File

@ -10,13 +10,13 @@ protocol CacheGraphMutating {
/// to the .xcframeworks in the cache, it mutates the graph to link the enry nodes against the .xcframeworks instead.
/// - Parameters:
/// - graph: Dependency graph.
/// - xcframeworks: Dictionary that maps targets with the paths to their cached .xcframeworks.
/// - precompiledFrameworks: Dictionary that maps targets with the paths to their cached `.framework`s or `.xcframework`s.
/// - source: Contains a list of targets that won't be replaced with their pre-compiled version from the cache.
func map(graph: Graph, xcframeworks: [TargetNode: AbsolutePath], sources: Set<String>) throws -> Graph
func map(graph: Graph, precompiledFrameworks: [TargetNode: AbsolutePath], sources: Set<String>) throws -> Graph
}
class CacheGraphMutator: CacheGraphMutating {
struct VisitedXCFramework {
struct VisitedPrecompiledFramework {
let path: AbsolutePath?
}
@ -25,54 +25,66 @@ class CacheGraphMutator: CacheGraphMutating {
/// Utility to parse an .xcframework from the filesystem and load it into memory.
private let xcframeworkLoader: XCFrameworkNodeLoading
/// Utility to parse a .framework from the filesystem and load it into memory.
private let frameworkLoader: FrameworkNodeLoading
/// Initializes the graph mapper with its attributes.
/// - Parameter xcframeworkLoader: Utility to parse an .xcframework from the filesystem and load it into memory.
init(xcframeworkLoader: XCFrameworkNodeLoading = XCFrameworkNodeLoader()) {
init(frameworkLoader: FrameworkNodeLoading = FrameworkNodeLoader(),
xcframeworkLoader: XCFrameworkNodeLoading = XCFrameworkNodeLoader())
{
self.frameworkLoader = frameworkLoader
self.xcframeworkLoader = xcframeworkLoader
}
// MARK: - CacheGraphMapping
public func map(graph: Graph, xcframeworks: [TargetNode: AbsolutePath], sources: Set<String>) throws -> Graph {
var visitedXCFrameworkPaths: [TargetNode: VisitedXCFramework?] = [:]
var loadedXCFrameworks: [AbsolutePath: XCFrameworkNode] = [:]
public func map(graph: Graph, precompiledFrameworks: [TargetNode: AbsolutePath], sources: Set<String>) throws -> Graph {
var visitedPrecompiledFrameworkPaths: [TargetNode: VisitedPrecompiledFramework?] = [:]
var loadedPrecompiledNodes: [AbsolutePath: PrecompiledNode] = [:]
let userSpecifiedSourceTargets = graph.targets.flatMap { $0.value }.filter { sources.contains($0.target.name) }
let userSpecifiedSourceTestTargets = userSpecifiedSourceTargets.flatMap { graph.testTargetsDependingOn(path: $0.path, name: $0.name) }
var sourceTargets: Set<TargetNode> = Set(userSpecifiedSourceTargets)
try (userSpecifiedSourceTargets + userSpecifiedSourceTestTargets)
.forEach { try visit(targetNode: $0,
xcframeworks: xcframeworks,
precompiledFrameworks: precompiledFrameworks,
sources: sources,
sourceTargets: &sourceTargets,
visitedXCFrameworkPaths: &visitedXCFrameworkPaths,
loadedXCFrameworks: &loadedXCFrameworks) }
visitedPrecompiledFrameworkPaths: &visitedPrecompiledFrameworkPaths,
loadedPrecompiledNodes: &loadedPrecompiledNodes) }
return treeShake(graph: graph, sourceTargets: sourceTargets)
// We mark them to be pruned during the tree-shaking
graph.targets.flatMap(\.value).forEach {
if !sourceTargets.contains($0) { $0.prune = true }
}
return graph
}
fileprivate func visit(targetNode: TargetNode,
xcframeworks: [TargetNode: AbsolutePath],
precompiledFrameworks: [TargetNode: AbsolutePath],
sources: Set<String>,
sourceTargets: inout Set<TargetNode>,
visitedXCFrameworkPaths: inout [TargetNode: VisitedXCFramework?],
loadedXCFrameworks: inout [AbsolutePath: XCFrameworkNode]) throws
visitedPrecompiledFrameworkPaths: inout [TargetNode: VisitedPrecompiledFramework?],
loadedPrecompiledNodes: inout [AbsolutePath: PrecompiledNode]) throws
{
sourceTargets.formUnion([targetNode])
targetNode.dependencies = try mapDependencies(targetNode.dependencies,
xcframeworks: xcframeworks,
precompiledFrameworks: precompiledFrameworks,
sources: sources,
sourceTargets: &sourceTargets,
visitedXCFrameworkPaths: &visitedXCFrameworkPaths,
loadedXCFrameworks: &loadedXCFrameworks)
visitedPrecompiledFrameworkPaths: &visitedPrecompiledFrameworkPaths,
loadedPrecompiledFrameworks: &loadedPrecompiledNodes)
}
// swiftlint:disable line_length
fileprivate func mapDependencies(_ dependencies: [GraphNode],
xcframeworks: [TargetNode: AbsolutePath],
precompiledFrameworks: [TargetNode: AbsolutePath],
sources: Set<String>,
sourceTargets: inout Set<TargetNode>,
visitedXCFrameworkPaths: inout [TargetNode: VisitedXCFramework?],
loadedXCFrameworks: inout [AbsolutePath: XCFrameworkNode]) throws -> [GraphNode]
visitedPrecompiledFrameworkPaths: inout [TargetNode: VisitedPrecompiledFramework?],
loadedPrecompiledFrameworks: inout [AbsolutePath: PrecompiledNode]) throws -> [GraphNode]
{
var newDependencies: [GraphNode] = []
try dependencies.forEach { dependency in
@ -82,114 +94,80 @@ class CacheGraphMutator: CacheGraphMutating {
return
}
// If the target cannot be replaced with its associated .xcframework we return
guard !sources.contains(targetDependency.target.name), let xcframeworkPath = xcframeworkPath(target: targetDependency,
xcframeworks: xcframeworks,
visitedXCFrameworkPaths: &visitedXCFrameworkPaths)
// If the target cannot be replaced with its associated .(xc)framework we return
guard !sources.contains(targetDependency.target.name), let precompiledFrameworkPath = precompiledFrameworkPath(target: targetDependency,
precompiledFrameworks: precompiledFrameworks,
visitedPrecompiledFrameworkPaths: &visitedPrecompiledFrameworkPaths)
else {
sourceTargets.formUnion([targetDependency])
targetDependency.dependencies = try mapDependencies(targetDependency.dependencies,
xcframeworks: xcframeworks,
precompiledFrameworks: precompiledFrameworks,
sources: sources,
sourceTargets: &sourceTargets,
visitedXCFrameworkPaths: &visitedXCFrameworkPaths,
loadedXCFrameworks: &loadedXCFrameworks)
visitedPrecompiledFrameworkPaths: &visitedPrecompiledFrameworkPaths,
loadedPrecompiledFrameworks: &loadedPrecompiledFrameworks)
newDependencies.append(targetDependency)
return
}
// We load the xcframework
let xcframework = try self.loadXCFramework(path: xcframeworkPath, loadedXCFrameworks: &loadedXCFrameworks)
// We load the .framework (or fallback on .xcframework)
let precompiledFramework: PrecompiledNode = try loadPrecompiledFramework(path: precompiledFrameworkPath, loadedPrecompiledFrameworks: &loadedPrecompiledFrameworks)
try mapDependencies(targetDependency.dependencies,
xcframeworks: xcframeworks,
precompiledFrameworks: precompiledFrameworks,
sources: sources,
sourceTargets: &sourceTargets,
visitedXCFrameworkPaths: &visitedXCFrameworkPaths,
loadedXCFrameworks: &loadedXCFrameworks).forEach { dependency in
visitedPrecompiledFrameworkPaths: &visitedPrecompiledFrameworkPaths,
loadedPrecompiledFrameworks: &loadedPrecompiledFrameworks).forEach { dependency in
if let frameworkDependency = dependency as? FrameworkNode {
xcframework.add(dependency: XCFrameworkNode.Dependency.framework(frameworkDependency))
precompiledFramework.add(dependency: PrecompiledNode.Dependency.framework(frameworkDependency))
} else if let xcframeworkDependency = dependency as? XCFrameworkNode {
xcframework.add(dependency: XCFrameworkNode.Dependency.xcframework(xcframeworkDependency))
precompiledFramework.add(dependency: PrecompiledNode.Dependency.xcframework(xcframeworkDependency))
} else {
// Static dependencies fall into this case.
// Those are now part of the precompiled xcframework and therefore we don't have to link against them.
// Those are now part of the precompiled (xc)framework and therefore we don't have to link against them.
}
}
newDependencies.append(xcframework)
newDependencies.append(precompiledFramework)
}
return newDependencies
}
func treeShake(graph: Graph, sourceTargets: Set<TargetNode>) -> Graph {
let targetReferences = Set(sourceTargets.map { TargetReference(projectPath: $0.path, name: $0.name) })
let projects = graph.projects.compactMap { (project) -> Project? in
let targets: [Target] = project.targets.compactMap { (target) -> Target? in
guard let targetNode = graph.target(path: project.path, name: target.name) else { return nil }
guard sourceTargets.contains(targetNode) else { return nil }
return target
}
if targets.isEmpty {
return nil
fileprivate func loadPrecompiledFramework(path: AbsolutePath, loadedPrecompiledFrameworks: inout [AbsolutePath: PrecompiledNode]) throws -> PrecompiledNode {
if let cachedFramework = loadedPrecompiledFrameworks[path] {
return cachedFramework
} else if let framework = try? frameworkLoader.load(path: path) {
loadedPrecompiledFrameworks[path] = framework
return framework
} else {
let schemes: [Scheme] = project.schemes.compactMap { scheme -> Scheme? in
let buildActionTargets = scheme.buildAction?.targets.filter { targetReferences.contains($0) } ?? []
// The scheme contains no buildable targets so we don't include it.
if buildActionTargets.isEmpty { return nil }
let testActionTargets = scheme.testAction?.targets.filter { targetReferences.contains($0.target) } ?? []
var scheme = scheme
var buildAction = scheme.buildAction
var testAction = scheme.testAction
buildAction?.targets = buildActionTargets
testAction?.targets = testActionTargets
scheme.buildAction = buildAction
scheme.testAction = testAction
return scheme
}
return project.with(targets: targets).with(schemes: schemes)
}
}
return graph
.with(projects: projects)
.with(targets: sourceTargets.reduce(into: [AbsolutePath: [TargetNode]]()) { acc, target in
var targets = acc[target.path, default: []]
targets.append(target)
acc[target.path] = targets
})
}
fileprivate func loadXCFramework(path: AbsolutePath, loadedXCFrameworks: inout [AbsolutePath: XCFrameworkNode]) throws -> XCFrameworkNode {
if let cachedXCFramework = loadedXCFrameworks[path] { return cachedXCFramework }
let xcframework = try xcframeworkLoader.load(path: path)
loadedXCFrameworks[path] = xcframework
loadedPrecompiledFrameworks[path] = xcframework
return xcframework
}
}
fileprivate func xcframeworkPath(target: TargetNode,
xcframeworks: [TargetNode: AbsolutePath],
visitedXCFrameworkPaths: inout [TargetNode: VisitedXCFramework?]) -> AbsolutePath?
fileprivate func precompiledFrameworkPath(target: TargetNode,
precompiledFrameworks: [TargetNode: AbsolutePath],
visitedPrecompiledFrameworkPaths: inout [TargetNode: VisitedPrecompiledFramework?]) -> AbsolutePath?
{
// Already visited
if let visited = visitedXCFrameworkPaths[target] { return visited?.path }
if let visited = visitedPrecompiledFrameworkPaths[target] { return visited?.path }
// The target doesn't have a cached xcframework
if xcframeworks[target] == nil {
visitedXCFrameworkPaths[target] = VisitedXCFramework(path: nil)
// The target doesn't have a cached .(xc)framework
if precompiledFrameworks[target] == nil {
visitedPrecompiledFrameworkPaths[target] = VisitedPrecompiledFramework(path: nil)
return nil
}
// The target can be replaced
else if let path = xcframeworks[target],
target.targetDependencies.allSatisfy({ xcframeworkPath(target: $0, xcframeworks: xcframeworks,
visitedXCFrameworkPaths: &visitedXCFrameworkPaths) != nil })
else if let path = precompiledFrameworks[target],
target.targetDependencies.allSatisfy({ precompiledFrameworkPath(target: $0,
precompiledFrameworks: precompiledFrameworks,
visitedPrecompiledFrameworkPaths: &visitedPrecompiledFrameworkPaths) != nil })
{
visitedXCFrameworkPaths[target] = VisitedXCFramework(path: path)
visitedPrecompiledFrameworkPaths[target] = VisitedPrecompiledFramework(path: path)
return path
} else {
visitedXCFrameworkPaths[target] = VisitedXCFramework(path: nil)
visitedPrecompiledFrameworkPaths[target] = VisitedPrecompiledFramework(path: nil)
return nil
}
}

View File

@ -17,7 +17,7 @@ public class CacheMapper: GraphMapping {
/// Cache graph mapper.
private let cacheGraphMutator: CacheGraphMutating
/// Configuration object
/// Configuration object.
private let config: Config
/// List of targets that will be generated as sources instead of pre-compiled targets from the cache.
@ -26,22 +26,28 @@ public class CacheMapper: GraphMapping {
/// Dispatch queue.
private let queue: DispatchQueue
/// The type of artifact that the hasher is configured with.
private let cacheOutputType: CacheOutputType
// MARK: - Init
public convenience init(config: Config,
cacheStorageProvider: CacheStorageProviding,
sources: Set<String>)
sources: Set<String>,
cacheOutputType: CacheOutputType)
{
self.init(config: config,
cache: Cache(storageProvider: cacheStorageProvider),
graphContentHasher: GraphContentHasher(),
sources: sources)
sources: sources,
cacheOutputType: cacheOutputType)
}
init(config: Config,
cache: CacheStoring,
graphContentHasher: GraphContentHashing,
sources: Set<String>,
cacheOutputType: CacheOutputType,
cacheGraphMutator: CacheGraphMutating = CacheGraphMutator(),
queue: DispatchQueue = CacheMapper.dispatchQueue())
{
@ -51,6 +57,7 @@ public class CacheMapper: GraphMapping {
self.queue = queue
self.cacheGraphMutator = cacheGraphMutator
self.sources = sources
self.cacheOutputType = cacheOutputType
}
// MARK: - GraphMapping
@ -70,7 +77,8 @@ public class CacheMapper: GraphMapping {
fileprivate func hashes(graph: Graph) -> Single<[TargetNode: String]> {
Single.create { (observer) -> Disposable in
do {
let hashes = try self.graphContentHasher.contentHashes(for: graph)
let hashes = try self.graphContentHasher.contentHashes(for: graph,
cacheOutputType: self.cacheOutputType)
observer(.success(hashes))
} catch {
observer(.error(error))
@ -83,7 +91,7 @@ public class CacheMapper: GraphMapping {
fileprivate func map(graph: Graph, hashes: [TargetNode: String], sources: Set<String>) -> Single<Graph> {
fetch(hashes: hashes).map { xcframeworkPaths in
try self.cacheGraphMutator.map(graph: graph,
xcframeworks: xcframeworkPaths,
precompiledFrameworks: xcframeworkPaths,
sources: sources)
}
}

View File

@ -0,0 +1,72 @@
import Foundation
import TSCBasic
import TuistCore
public final class CacheTreeShakingGraphMapper: GraphMapping {
public init() {}
public func map(graph: Graph) throws -> (Graph, [SideEffectDescriptor]) {
let sourceTargets: Set<TargetReference> = graph.targets.reduce(into: Set<TargetReference>()) { acc, next in
acc.formUnion(next.value.filter { !$0.prune }.map { TargetReference(projectPath: $0.path, name: $0.name) })
}
// If the number of source targets matches the number of targets in the graph there's nothing to be pruned.
if sourceTargets.count == graph.targets.flatMap(\.value).count { return (graph, []) }
let projects = graph.projects.compactMap { (project) -> Project? in
let targets = self.treeShake(targets: project.targets,
path: project.path,
graph: graph,
sourceTargets: sourceTargets)
// If the project has no targets we remove the project.
if targets.isEmpty {
return nil
} else {
let schemes = self.treeShake(schemes: project.schemes,
sourceTargets: sourceTargets)
return project.with(targets: targets).with(schemes: schemes)
}
}
let graph = graph
.with(projects: projects)
.with(targets: sourceTargets.reduce(into: [AbsolutePath: [TargetNode]]()) { acc, targetReference in
var targets = acc[targetReference.projectPath, default: []]
if let target = graph.target(path: targetReference.projectPath, name: targetReference.name) {
targets.append(target)
}
acc[targetReference.projectPath] = targets
})
return (graph, [])
}
fileprivate func treeShake(targets: [Target], path: AbsolutePath, graph: Graph, sourceTargets: Set<TargetReference>) -> [Target] {
targets.compactMap { (target) -> Target? in
guard let targetNode = graph.target(path: path, name: target.name) else { return nil }
let targetReference = TargetReference(projectPath: targetNode.path, name: targetNode.name)
guard sourceTargets.contains(targetReference) else { return nil }
return target
}
}
fileprivate func treeShake(schemes: [Scheme], sourceTargets: Set<TargetReference>) -> [Scheme] {
schemes.compactMap { scheme -> Scheme? in
let buildActionTargets = scheme.buildAction?.targets.filter { sourceTargets.contains($0) } ?? []
// The scheme contains no buildable targets so we don't include it.
if buildActionTargets.isEmpty { return nil }
let testActionTargets = scheme.testAction?.targets.filter { sourceTargets.contains($0.target) } ?? []
var scheme = scheme
var buildAction = scheme.buildAction
var testAction = scheme.testAction
buildAction?.targets = buildActionTargets
testAction?.targets = testActionTargets
scheme.buildAction = buildAction
scheme.testAction = testAction
return scheme
}
}
}

View File

@ -0,0 +1,23 @@
import TSCBasic
import TuistCore
public protocol CacheArtifactBuilding {
/// Returns the type of artifact that the concrete builder processes.
var cacheOutputType: CacheOutputType { get }
/// Builds a given target and outputs the cacheable artifact into the given directory.
///
/// - Parameters:
/// - workspacePath: Path to the generated .xcworkspace that contains the given target.
/// - target: Target whose artifact will be generated.
/// - into: The directory into which the output artifacts will be copied.
func build(workspacePath: AbsolutePath, target: Target, into outputDirectory: AbsolutePath) throws
/// Builds a given target and outputs the cacheable artifact into the given directory.
///
/// - Parameters:
/// - projectPath: Path to the generated .xcodeproj that contains the given target.
/// - target: Target whose .(xc)framework will be generated.
/// - into: The directory into which the output artifacts will be copied.
func build(projectPath: AbsolutePath, target: Target, into outputDirectory: AbsolutePath) throws
}

View File

@ -0,0 +1,24 @@
import TuistSupport
enum CacheBinaryBuilderError: FatalError {
case nonFrameworkTargetForXCFramework(String)
case nonFrameworkTargetForFramework(String)
/// Error type.
var type: ErrorType {
switch self {
case .nonFrameworkTargetForXCFramework: return .abort
case .nonFrameworkTargetForFramework: return .abort
}
}
/// Error description.
var description: String {
switch self {
case let .nonFrameworkTargetForXCFramework(name):
return "Can't generate an .xcframework from the target '\(name)' because it's not a framework target"
case let .nonFrameworkTargetForFramework(name):
return "Can't generate a .framework from the target '\(name)' because it's not a framework target"
}
}
}

View File

@ -0,0 +1,209 @@
import Foundation
import RxBlocking
import RxSwift
import TSCBasic
import TuistCore
import TuistSupport
public enum CacheFrameworkBuilderError: FatalError {
case builtProductsDirectoryNotFound(targetName: String)
case frameworkNotFound(name: String, derivedDataPath: AbsolutePath)
case deviceNotFound(platform: String)
public var description: String {
switch self {
case let .builtProductsDirectoryNotFound(targetName):
return "Couldn't find the built products directory for target '\(targetName)'."
case let .frameworkNotFound(name, derivedDataPath):
return "Couldn't find framework '\(name)' in the derived data directory: \(derivedDataPath.pathString)"
case let .deviceNotFound(platform):
return "Couldn't find an available device for platform: '\(platform)'"
}
}
public var type: ErrorType {
switch self {
case .builtProductsDirectoryNotFound: return .bug
case .frameworkNotFound: return .bug
case .deviceNotFound: return .bug
}
}
}
public final class CacheFrameworkBuilder: CacheArtifactBuilding {
// MARK: - Attributes
/// Xcode build controller instance to run xcodebuild commands.
private let xcodeBuildController: XcodeBuildControlling
/// Simulator controller.
private let simulatorController: SimulatorControlling
/// Developer's environment.
private let developerEnvironment: DeveloperEnvironmenting
// MARK: - Init
/// Initialzies the builder.
/// - Parameters:
/// - xcodeBuildController: Xcode build controller.
/// - simulatorController: Simulator controller.
/// - developerEnvironment: Developer environment.
public init(xcodeBuildController: XcodeBuildControlling,
simulatorController: SimulatorControlling = SimulatorController(),
developerEnvironment: DeveloperEnvironmenting = DeveloperEnvironment.shared)
{
self.xcodeBuildController = xcodeBuildController
self.simulatorController = simulatorController
self.developerEnvironment = developerEnvironment
}
// MARK: - ArtifactBuilding
/// Returns the type of artifact that the concrete builder processes
public var cacheOutputType: CacheOutputType = .framework
public func build(workspacePath: AbsolutePath,
target: Target,
into outputDirectory: AbsolutePath) throws
{
try build(.workspace(workspacePath),
target: target,
into: outputDirectory)
}
public func build(projectPath: AbsolutePath,
target: Target,
into outputDirectory: AbsolutePath) throws
{
try build(.project(projectPath),
target: target,
into: outputDirectory)
}
// MARK: - Fileprivate
fileprivate func build(_ projectTarget: XcodeBuildTarget,
target: Target,
into outputDirectory: AbsolutePath) throws
{
guard target.product.isFramework else {
throw CacheBinaryBuilderError.nonFrameworkTargetForFramework(target.name)
}
let scheme = target.name.spm_shellEscaped()
// Create temporary directories
let builtProductsDirFingerprint = String.random()
logger.notice("Building .framework for \(target.name)...", metadata: .section)
let sdk = self.sdk(target: target)
let configuration = "Debug" // TODO: Is it available?
let arguments = try self.arguments(target: target,
sdk: sdk,
configuration: configuration,
builtProductsDirFingerprint: builtProductsDirFingerprint)
try xcodebuild(
projectTarget: projectTarget,
scheme: scheme,
target: target,
arguments: arguments
)
try exportFrameworksAndDSYMs(into: outputDirectory,
target: target,
builtProductsDirFingerprint: builtProductsDirFingerprint)
}
fileprivate func arguments(target: Target,
sdk: String,
configuration: String,
builtProductsDirFingerprint: String) throws -> [XcodeBuildArgument]
{
try destination(target: target)
.map { (destination: String) -> [XcodeBuildArgument] in
[
.sdk(sdk),
.configuration(configuration),
.buildSetting("DEBUG_INFORMATION_FORMAT", "dwarf-with-dsym"),
.buildSetting("GCC_GENERATE_DEBUGGING_SYMBOLS", "YES"),
.buildSetting(target.targetLocatorBuildPhaseVariable, builtProductsDirFingerprint),
.destination(destination),
]
}
.toBlocking()
.single()
}
/// https://www.mokacoding.com/blog/xcodebuild-destination-options/
/// https://www.mokacoding.com/blog/how-to-always-run-latest-simulator-cli/
fileprivate func destination(target: Target) -> Single<String> {
var platform: Platform!
switch target.platform {
case .iOS: platform = .iOS
case .watchOS: platform = .watchOS
case .tvOS: platform = .tvOS
case .macOS: return .just("platform=OS X,arch=x86_64")
}
return simulatorController.devicesAndRuntimes()
.map { (simulatorsAndRuntimes) -> [SimulatorDevice] in
simulatorsAndRuntimes
.filter { $0.runtime.isAvailable && $0.runtime.name.contains(platform.caseValue) }
.map { $0.device }
}
.flatMap { (devices) -> Single<String> in
if let device = devices.first {
let destination = "platform=\(platform.caseValue) Simulator,name=\(device.name),OS=latest"
return .just(destination)
} else {
return .error(CacheFrameworkBuilderError.deviceNotFound(platform: target.platform.caseValue))
}
}
}
fileprivate func sdk(target: Target) -> String {
if target.platform == .macOS {
return target.platform.xcodeDeviceSDK
} else {
return target.platform.xcodeSimulatorSDK!
}
}
fileprivate func xcodebuild(projectTarget: XcodeBuildTarget,
scheme: String,
target: Target,
arguments: [XcodeBuildArgument]) throws
{
_ = try xcodeBuildController.build(projectTarget,
scheme: scheme,
clean: false,
arguments: arguments)
.printFormattedOutput()
.do(onSubscribed: {
logger.notice("Building \(target.name) as .framework...", metadata: .subsection)
})
.ignoreElements()
.toBlocking()
.last()
}
fileprivate func exportFrameworksAndDSYMs(into outputDirectory: AbsolutePath,
target: Target,
builtProductsDirFingerprint: String) throws
{
let globPattern = "**/.\(builtProductsDirFingerprint).tuist"
let derivedDataPath = developerEnvironment.derivedDataDirectory
guard let directory = FileHandler.shared.glob(derivedDataPath, glob: globPattern).first?.parentDirectory else {
throw CacheFrameworkBuilderError.builtProductsDirectoryNotFound(targetName: target.name)
}
guard let framework = FileHandler.shared.glob(directory, glob: target.productNameWithExtension).first else {
throw CacheFrameworkBuilderError.frameworkNotFound(name: target.productNameWithExtension, derivedDataPath: derivedDataPath)
}
let dsyms = FileHandler.shared.glob(directory, glob: "\(target.productNameWithExtension).dSYM")
try FileHandler.shared.copy(from: framework, to: outputDirectory.appending(component: framework.basename))
try dsyms.forEach { dsym in
try FileHandler.shared.copy(from: dsym, to: outputDirectory.appending(component: dsym.basename))
}
}
}

View File

@ -0,0 +1,159 @@
import Foundation
import RxBlocking
import RxSwift
import TSCBasic
import TuistCore
import TuistSupport
public final class CacheXCFrameworkBuilder: CacheArtifactBuilding {
// MARK: - Attributes
/// Xcode build controller instance to run xcodebuild commands.
private let xcodeBuildController: XcodeBuildControlling
// MARK: - Init
/// Initializes the builder.
/// - Parameter xcodeBuildController: Xcode build controller instance to run xcodebuild commands.
public init(xcodeBuildController: XcodeBuildControlling) {
self.xcodeBuildController = xcodeBuildController
}
// MARK: - ArtifactBuilding
/// Returns the type of artifact that the concrete builder processes
public var cacheOutputType: CacheOutputType = .xcframework
public func build(workspacePath: AbsolutePath,
target: Target,
into outputDirectory: AbsolutePath) throws
{
try build(.workspace(workspacePath),
target: target,
into: outputDirectory)
}
public func build(projectPath: AbsolutePath,
target: Target,
into outputDirectory: AbsolutePath) throws
{
try build(.project(projectPath),
target: target,
into: outputDirectory)
}
// MARK: - Fileprivate
// swiftlint:disable:next function_body_length
fileprivate func build(_ projectTarget: XcodeBuildTarget,
target: Target,
into outputDirectory: AbsolutePath) throws
{
guard target.product.isFramework else {
throw CacheBinaryBuilderError.nonFrameworkTargetForXCFramework(target.name)
}
let scheme = target.name.spm_shellEscaped()
// Create temporary directories
return try FileHandler.shared.inTemporaryDirectory { temporaryDirectory in
logger.notice("Building .xcframework for \(target.name)...", metadata: .section)
// Build for the simulator
var simulatorArchivePath: AbsolutePath?
if target.platform.hasSimulators {
simulatorArchivePath = temporaryDirectory.appending(component: "simulator.xcarchive")
try simulatorBuild(
projectTarget: projectTarget,
scheme: scheme,
target: target,
archivePath: simulatorArchivePath!
)
}
// Build for the device - if required
let deviceArchivePath = temporaryDirectory.appending(component: "device.xcarchive")
try deviceBuild(
projectTarget: projectTarget,
scheme: scheme,
target: target,
archivePath: deviceArchivePath
)
// Build the xcframework
var frameworkpaths = [AbsolutePath]()
if let simulatorArchivePath = simulatorArchivePath {
frameworkpaths.append(frameworkPath(fromArchivePath: simulatorArchivePath, productName: target.productName))
}
frameworkpaths.append(frameworkPath(fromArchivePath: deviceArchivePath, productName: target.productName))
let xcframeworkPath = outputDirectory.appending(component: "\(target.productName).xcframework")
try buildXCFramework(frameworks: frameworkpaths, output: xcframeworkPath, target: target)
try FileHandler.shared.move(from: xcframeworkPath, to: outputDirectory.appending(component: xcframeworkPath.basename))
}
}
fileprivate func buildXCFramework(frameworks: [AbsolutePath], output: AbsolutePath, target: Target) throws {
_ = try xcodeBuildController.createXCFramework(frameworks: frameworks, output: output)
.do(onSubscribed: {
logger.notice("Exporting xcframework for \(target.platform.caseValue)", metadata: .subsection)
})
.toBlocking()
.single()
}
fileprivate func deviceBuild(projectTarget: XcodeBuildTarget,
scheme: String,
target: Target,
archivePath: AbsolutePath) throws
{
// Without the BUILD_LIBRARY_FOR_DISTRIBUTION argument xcodebuild doesn't generate the .swiftinterface file
_ = try xcodeBuildController.archive(projectTarget,
scheme: scheme,
clean: false,
archivePath: archivePath,
arguments: [
.sdk(target.platform.xcodeDeviceSDK),
.buildSetting("SKIP_INSTALL", "NO"),
.buildSetting("BUILD_LIBRARY_FOR_DISTRIBUTION", "YES"),
])
.printFormattedOutput()
.do(onSubscribed: {
logger.notice("Building \(target.name) for device...", metadata: .subsection)
})
.ignoreElements()
.toBlocking()
.last()
}
fileprivate func simulatorBuild(projectTarget: XcodeBuildTarget,
scheme: String,
target: Target,
archivePath: AbsolutePath) throws
{
// Without the BUILD_LIBRARY_FOR_DISTRIBUTION argument xcodebuild doesn't generate the .swiftinterface file
_ = try xcodeBuildController.archive(projectTarget,
scheme: scheme,
clean: false,
archivePath: archivePath,
arguments: [
.sdk(target.platform.xcodeSimulatorSDK!),
.buildSetting("SKIP_INSTALL", "NO"),
.buildSetting("BUILD_LIBRARY_FOR_DISTRIBUTION", "YES"),
])
.printFormattedOutput()
.do(onSubscribed: {
logger.notice("Building \(target.name) for simulator...", metadata: .subsection)
})
.ignoreElements()
.toBlocking()
.last()
}
/// Returns the path to the framework inside the archive.
/// - Parameters:
/// - archivePath: Path to the .xcarchive.
/// - productName: Product name.
fileprivate func frameworkPath(fromArchivePath archivePath: AbsolutePath, productName: String) -> AbsolutePath {
archivePath.appending(RelativePath("Products/Library/Frameworks/\(productName).framework"))
}
}

View File

@ -1,189 +0,0 @@
import Foundation
import RxSwift
import TSCBasic
import TuistCore
import TuistSupport
enum XCFrameworkBuilderError: FatalError {
case nonFrameworkTarget(String)
/// Error type.
var type: ErrorType {
switch self {
case .nonFrameworkTarget: return .abort
}
}
/// Error description.
var description: String {
switch self {
case let .nonFrameworkTarget(name):
return "Can't generate an .xcframework from the target '\(name)' because it's not a framework target"
}
}
}
public protocol XCFrameworkBuilding {
/// Returns an observable build an xcframework for the given target.
/// The target must have framework as product.
///
/// - Parameters:
/// - workspacePath: Path to the generated .xcworkspace that contains the given target.
/// - target: Target whose .xcframework will be generated.
/// - includeDeviceArch: Define whether the .xcframework will also contain the target built for devices (it only contains the target built for simulators by default).
/// - Returns: Path to the compiled .xcframework.
func build(workspacePath: AbsolutePath, target: Target, includeDeviceArch: Bool) throws -> Observable<AbsolutePath>
/// Returns an observable to build an xcframework for the given target.
/// The target must have framework as product.
///
/// - Parameters:
/// - projectPath: Path to the generated .xcodeproj that contains the given target.
/// - target: Target whose .xcframework will be generated.
/// - includeDeviceArch: Define whether the .xcframework will also contain the target built for devices (it only contains the target built for simulators by default).
/// - Returns: Path to the compiled .xcframework.
func build(projectPath: AbsolutePath, target: Target, includeDeviceArch: Bool) throws -> Observable<AbsolutePath>
}
public final class XCFrameworkBuilder: XCFrameworkBuilding {
// MARK: - Attributes
/// Xcode build controller instance to run xcodebuild commands.
private let xcodeBuildController: XcodeBuildControlling
// MARK: - Init
/// Initializes the builder.
/// - Parameter xcodeBuildController: Xcode build controller instance to run xcodebuild commands.
public init(xcodeBuildController: XcodeBuildControlling) {
self.xcodeBuildController = xcodeBuildController
}
// MARK: - XCFrameworkBuilding
public func build(workspacePath: AbsolutePath, target: Target, includeDeviceArch: Bool) throws -> Observable<AbsolutePath> {
try build(.workspace(workspacePath), target: target, includeDeviceArch: includeDeviceArch)
}
public func build(projectPath: AbsolutePath, target: Target, includeDeviceArch: Bool) throws -> Observable<AbsolutePath> {
try build(.project(projectPath), target: target, includeDeviceArch: includeDeviceArch)
}
// MARK: - Fileprivate
fileprivate func deviceBuild(projectTarget: XcodeBuildTarget,
scheme: String,
target: Target,
deviceArchivePath: AbsolutePath) -> Observable<SystemEvent<XcodeBuildOutput>>
{
// Without the BUILD_LIBRARY_FOR_DISTRIBUTION argument xcodebuild doesn't generate the .swiftinterface file
xcodeBuildController.archive(projectTarget,
scheme: scheme,
clean: false,
archivePath: deviceArchivePath,
arguments: [
.sdk(target.platform.xcodeDeviceSDK),
.buildSetting("SKIP_INSTALL", "NO"),
.buildSetting("BUILD_LIBRARY_FOR_DISTRIBUTION", "YES"),
])
.printFormattedOutput()
.do(onSubscribed: {
logger.notice("Building \(target.name) for device...", metadata: .subsection)
})
}
fileprivate func simulatorBuild(projectTarget: XcodeBuildTarget,
scheme: String,
target: Target,
simulatorArchivePath: AbsolutePath) -> Observable<SystemEvent<XcodeBuildOutput>>
{
// Without the BUILD_LIBRARY_FOR_DISTRIBUTION argument xcodebuild doesn't generate the .swiftinterface file
xcodeBuildController.archive(projectTarget,
scheme: scheme,
clean: false,
archivePath: simulatorArchivePath,
arguments: [
.sdk(target.platform.xcodeSimulatorSDK!),
.buildSetting("SKIP_INSTALL", "NO"),
.buildSetting("BUILD_LIBRARY_FOR_DISTRIBUTION", "YES"),
])
.printFormattedOutput()
.do(onSubscribed: {
logger.notice("Building \(target.name) for simulator...", metadata: .subsection)
})
}
// swiftlint:disable:next function_body_length
fileprivate func build(_ projectTarget: XcodeBuildTarget, target: Target, includeDeviceArch: Bool) throws -> Observable<AbsolutePath> {
guard target.product.isFramework else {
throw XCFrameworkBuilderError.nonFrameworkTarget(target.name)
}
let scheme = target.name.spm_shellEscaped()
// Create temporary directories
return try withTemporaryDirectories { outputDirectory, temporaryPath in
logger.notice("Building .xcframework for \(target.name)...", metadata: .section)
// Build for the simulator
var simulatorArchiveObservable: Observable<SystemEvent<XcodeBuildOutput>>
var simulatorArchivePath: AbsolutePath?
if target.platform.hasSimulators {
simulatorArchivePath = temporaryPath.appending(component: "simulator.xcarchive")
simulatorArchiveObservable = simulatorBuild(
projectTarget: projectTarget,
scheme: scheme,
target: target,
simulatorArchivePath: simulatorArchivePath!
)
} else {
simulatorArchiveObservable = Observable.empty()
}
// Build for the device - if required
let deviceArchivePath = temporaryPath.appending(component: "device.xcarchive")
var deviceArchiveObservable: Observable<SystemEvent<XcodeBuildOutput>>
if includeDeviceArch {
deviceArchiveObservable = deviceBuild(
projectTarget: projectTarget,
scheme: scheme,
target: target,
deviceArchivePath: deviceArchivePath
)
} else {
deviceArchiveObservable = Observable.empty()
}
// Build the xcframework
var frameworkpaths: [AbsolutePath] = [AbsolutePath]()
if let simulatorArchivePath = simulatorArchivePath {
frameworkpaths.append(frameworkPath(fromArchivePath: simulatorArchivePath, productName: target.productName))
}
if includeDeviceArch {
frameworkpaths.append(frameworkPath(fromArchivePath: deviceArchivePath, productName: target.productName))
}
let xcframeworkPath = outputDirectory.appending(component: "\(target.productName).xcframework")
let xcframeworkObservable = xcodeBuildController.createXCFramework(frameworks: frameworkpaths, output: xcframeworkPath)
.do(onSubscribed: {
logger.notice("Exporting xcframework for \(target.platform.caseValue)", metadata: .subsection)
})
return deviceArchiveObservable
.concat(simulatorArchiveObservable)
.concat(xcframeworkObservable)
.ignoreElements()
.andThen(Observable.just(xcframeworkPath))
.do(afterCompleted: {
try FileHandler.shared.delete(temporaryPath)
})
}
}
/// Returns the path to the framework inside the archive.
/// - Parameters:
/// - archivePath: Path to the .xcarchive.
/// - productName: Product name.
fileprivate func frameworkPath(fromArchivePath archivePath: AbsolutePath, productName: String) -> AbsolutePath {
archivePath.appending(RelativePath("Products/Library/Frameworks/\(productName).framework"))
}
}

View File

@ -0,0 +1,52 @@
import Foundation
import RxSwift
import TSCBasic
import TuistCache
import TuistCore
import TuistSupportTesting
public final class MockCacheArtifactBuilder: CacheArtifactBuilding {
public init() {}
public var invokedCacheOutputTypeGetter = false
public var invokedCacheOutputTypeGetterCount = 0
public var stubbedCacheOutputType: CacheOutputType!
public var cacheOutputType: CacheOutputType {
invokedCacheOutputTypeGetter = true
invokedCacheOutputTypeGetterCount += 1
return stubbedCacheOutputType
}
public var invokedBuildWorkspacePath = false
public var invokedBuildWorkspacePathCount = 0
public var invokedBuildWorkspacePathParameters: (workspacePath: AbsolutePath, target: Target, outputDirectory: AbsolutePath)?
public var invokedBuildWorkspacePathParametersList = [(workspacePath: AbsolutePath, target: Target, outputDirectory: AbsolutePath)]()
public var stubbedBuildWorkspacePathError: Error?
public func build(workspacePath: AbsolutePath, target: Target, into outputDirectory: AbsolutePath) throws {
invokedBuildWorkspacePath = true
invokedBuildWorkspacePathCount += 1
invokedBuildWorkspacePathParameters = (workspacePath, target, outputDirectory)
invokedBuildWorkspacePathParametersList.append((workspacePath, target, outputDirectory))
if let error = stubbedBuildWorkspacePathError {
throw error
}
}
public var invokedBuildProjectPath = false
public var invokedBuildProjectPathCount = 0
public var invokedBuildProjectPathParameters: (projectPath: AbsolutePath, target: Target, outputDirectory: AbsolutePath)?
public var invokedBuildProjectPathParametersList = [(projectPath: AbsolutePath, target: Target, outputDirectory: AbsolutePath)]()
public var stubbedBuildProjectPathError: Error?
public func build(projectPath: AbsolutePath, target: Target, into outputDirectory: AbsolutePath) throws {
invokedBuildProjectPath = true
invokedBuildProjectPathCount += 1
invokedBuildProjectPathParameters = (projectPath, target, outputDirectory)
invokedBuildProjectPathParametersList.append((projectPath, target, outputDirectory))
if let error = stubbedBuildProjectPathError {
throw error
}
}
}

View File

@ -30,10 +30,10 @@ public final class MockCacheStorage: CacheStoring {
}
}
var storeStub: ((_ hash: String, _ xcframeworkPath: AbsolutePath) -> Void)?
public func store(hash: String, xcframeworkPath: AbsolutePath) -> Completable {
var storeStub: ((_ hash: String, _ paths: [AbsolutePath]) -> Void)?
public func store(hash: String, paths: [AbsolutePath]) -> Completable {
if let storeStub = storeStub {
storeStub(hash, xcframeworkPath)
storeStub(hash, paths)
}
return Completable.empty()
}

View File

@ -1,42 +0,0 @@
import Foundation
import RxSwift
import TSCBasic
import TuistCache
import TuistCore
public final class MockXCFrameworkBuilder: XCFrameworkBuilding {
public var buildProjectArgs: [(projectPath: AbsolutePath, target: Target, includeDeviceArch: Bool)] = []
public var buildWorkspaceArgs: [(workspacePath: AbsolutePath, target: Target, includeDeviceArch: Bool)] = []
public var buildProjectStub: ((AbsolutePath, Target) -> Result<AbsolutePath, Error>)?
public var buildWorkspaceStub: ((AbsolutePath, Target) -> Result<AbsolutePath, Error>)?
public init() {}
public func build(projectPath: AbsolutePath, target: Target, includeDeviceArch: Bool) throws -> Observable<AbsolutePath> {
buildProjectArgs.append((projectPath: projectPath, target: target, includeDeviceArch: includeDeviceArch))
if let buildProjectStub = buildProjectStub {
switch buildProjectStub(projectPath, target) {
case let .failure(error):
return Observable.error(error)
case let .success(path):
return Observable.just(path)
}
} else {
return Observable.just(AbsolutePath.root)
}
}
public func build(workspacePath: AbsolutePath, target: Target, includeDeviceArch: Bool) throws -> Observable<AbsolutePath> {
buildWorkspaceArgs.append((workspacePath: workspacePath, target: target, includeDeviceArch: includeDeviceArch))
if let buildWorkspaceStub = buildWorkspaceStub {
switch buildWorkspaceStub(workspacePath, target) {
case let .failure(error):
return Observable.error(error)
case let .success(path):
return Observable.just(path)
}
} else {
return Observable.just(AbsolutePath.root)
}
}
}

View File

@ -3,23 +3,23 @@ import TuistCore
@testable import TuistCache
public final class MockGraphContentHasher: GraphContentHashing {
public init() {}
public var invokedContentHashes = false
public var invokedContentHashesCount = 0
public var invokedContentHashesParameters: (graph: TuistCore.Graph, Void)?
public var invokedContentHashesParametersList = [(graph: TuistCore.Graph, Void)]()
public var invokedContentHashesParameters: (graph: TuistCore.Graph, cacheOutputType: CacheOutputType)?
public var invokedContentHashesParametersList = [(graph: TuistCore.Graph, cacheOutputType: CacheOutputType)]()
public var stubbedContentHashesError: Error?
public var contentHashesStub: [TargetNode: String]! = [:]
public var stubbedContentHashesResult: [TargetNode: String]! = [:]
public func contentHashes(for graph: TuistCore.Graph) throws -> [TargetNode: String] {
public init() {}
public func contentHashes(for graph: TuistCore.Graph, cacheOutputType: CacheOutputType) throws -> [TargetNode: String] {
invokedContentHashes = true
invokedContentHashesCount += 1
invokedContentHashesParameters = (graph, ())
invokedContentHashesParametersList.append((graph, ()))
invokedContentHashesParameters = (graph, cacheOutputType)
invokedContentHashesParametersList.append((graph, cacheOutputType))
if let error = stubbedContentHashesError {
throw error
}
return contentHashesStub
return stubbedContentHashesResult
}
}

View File

@ -3,6 +3,11 @@ import RxSwift
import TSCBasic
import TuistSupport
public enum XcodeBuildDestination: Equatable {
case device(String)
case mac
}
public protocol XcodeBuildControlling {
/// Returns an observable to build the given project using xcodebuild.
/// - Parameters:
@ -15,6 +20,20 @@ public protocol XcodeBuildControlling {
clean: Bool,
arguments: [XcodeBuildArgument]) -> Observable<SystemEvent<XcodeBuildOutput>>
/// Returns an observable to test the given project using xcodebuild.
/// - Parameters:
/// - target: The project or workspace to be built.
/// - scheme: The scheme of the project that should be built.
/// - clean: True if xcodebuild should clean the project before building.
/// - arguments: Extra xcodebuild arguments.
func test(
_ target: XcodeBuildTarget,
scheme: String,
clean: Bool,
destination: XcodeBuildDestination,
arguments: [XcodeBuildArgument]
) -> Observable<SystemEvent<XcodeBuildOutput>>
/// Returns an observable that archives the given project using xcodebuild.
/// - Parameters:
/// - target: The project or workspace to be archived.

View File

@ -0,0 +1,19 @@
import Foundation
/// An enum that represents the type of output that the caching feature can work with.
public enum CacheOutputType: CustomStringConvertible {
/// Frameworks built for the simulator.
case framework
/// XCFrameworks built for the simulator and device.
case xcframework
public var description: String {
switch self {
case .framework:
return "framework"
case .xcframework:
return "xcframework"
}
}
}

View File

@ -33,7 +33,10 @@ public class Graph: Encodable, Equatable {
/// The entry nodes of the graph.
public let entryNodes: [GraphNode]
/// Dictionary whose keys are paths to directories where projects are defined, and the values are the representation of the projects.
/// Workspace of the graph
public let workspace: Workspace
/// Projects of the graph
public let projects: [Project]
/// Dictionary whose keys are paths to directories where projects are defined, and the values are the CocoaPods nodes define in them.
@ -54,31 +57,48 @@ public class Graph: Encodable, Equatable {
/// Dictionary whose keys are path to directories where projects are defined, and the values are target nodes defined in them.
public let targets: [AbsolutePath: [TargetNode]]
/// Schemes of the graph
public var schemes: [Scheme] {
projects.flatMap(\.schemes) + workspace.schemes
}
// MARK: - Init
convenience init(name: String, entryPath: AbsolutePath, cache: GraphLoaderCaching, entryNodes: [GraphNode]) {
self.init(name: name,
convenience init(
name: String,
entryPath: AbsolutePath,
cache: GraphLoaderCaching,
entryNodes: [GraphNode],
workspace: Workspace
) {
self.init(
name: name,
entryPath: entryPath,
entryNodes: entryNodes,
workspace: workspace,
projects: Array(cache.projects.values),
cocoapods: Array(cache.cocoapodsNodes.values),
packages: Array(cache.packages.flatMap { $0.value }),
precompiled: Array(cache.precompiledNodes.values),
targets: cache.targetNodes.mapValues { Array($0.values) })
targets: cache.targetNodes.mapValues { Array($0.values) }
)
}
public init(name: String,
public init(
name: String,
entryPath: AbsolutePath,
entryNodes: [GraphNode],
workspace: Workspace,
projects: [Project],
cocoapods: [CocoaPodsNode],
packages: [PackageNode],
precompiled: [PrecompiledNode],
targets: [AbsolutePath: [TargetNode]])
{
targets: [AbsolutePath: [TargetNode]]
) {
self.name = name
self.entryPath = entryPath
self.entryNodes = entryNodes
self.workspace = workspace
self.projects = projects
self.cocoapods = cocoapods
self.packages = packages
@ -186,7 +206,7 @@ public class Graph: Encodable, Equatable {
/// - Parameters:
/// - path: Path to the directory where the project that defines the target is located.
/// - name: Name of the target.
public func linkableDependencies(path: AbsolutePath, name: String) -> [GraphDependencyReference] {
public func linkableDependencies(path: AbsolutePath, name: String) throws -> [GraphDependencyReference] {
guard let targetNode = findTargetNode(path: path, name: name) else {
return []
}
@ -205,6 +225,13 @@ public class Graph: Encodable, Equatable {
references = references.union(transitiveSystemLibraries)
}
if targetNode.target.isAppClip {
let path = try SDKNode.appClip(status: .required).path
references.insert(GraphDependencyReference.sdk(path: path,
status: .required,
source: .system))
}
let directSystemLibrariesAndFrameworks = targetNode.sdkDependencies.map {
GraphDependencyReference.sdk(path: $0.path, status: $0.status, source: $0.source)
}
@ -357,7 +384,7 @@ public class Graph: Encodable, Equatable {
if let hostApp = hostApplication(for: targetNode) {
references.subtract(try embeddableFrameworks(path: hostApp.path, name: hostApp.name))
} else {
references.subtract(precompiledFrameworks)
references = []
}
}
@ -387,7 +414,7 @@ public class Graph: Encodable, Equatable {
/// - Parameter project: Project whose dependency references will be returned.
public func allDependencyReferences(for project: Project) throws -> [GraphDependencyReference] {
let linkableDependencies = try project.targets.flatMap {
self.linkableDependencies(path: project.path, name: $0.name)
try self.linkableDependencies(path: project.path, name: $0.name)
}
let embeddableDependencies = try project.targets.flatMap {
@ -419,6 +446,14 @@ public class Graph: Encodable, Equatable {
.filter { validProducts.contains($0.target.product) }
}
public func appClipsDependency(path: AbsolutePath, name: String) -> TargetNode? {
guard let targetNode = findTargetNode(path: path, name: name) else {
return nil
}
return targetNode.targetDependencies.first { $0.target.product == .appClip }
}
/// Depth-first search (DFS) is an algorithm for traversing graph data structures. It starts at a source node
/// and explores as far as possible along each branch before backtracking.
///
@ -475,7 +510,7 @@ public class Graph: Encodable, Equatable {
stack.push(child)
}
} else if let frameworkNode = node as? FrameworkNode {
for child in frameworkNode.dependencies where !visited.contains(child) {
for child in frameworkNode.dependencies.map(\.node) where !visited.contains(child) {
stack.push(child)
}
}
@ -531,31 +566,60 @@ public class Graph: Encodable, Equatable {
} ?? nil
}
/// - Returns: Host application for a given `targetNode`, if it exists
public func hostApplication(for targetNode: TargetNode) -> TargetNode? {
targetDependencies(path: targetNode.path, name: targetNode.name)
.first(where: { $0.target.product == .app })
}
/// Returns a copy of the graph with the given projects set.
/// - Parameter projects: Projects to be set to the copy.
public func with(projects: [Project]) -> Graph {
Graph(name: name,
Graph(
name: name,
entryPath: entryPath,
entryNodes: entryNodes,
workspace: workspace,
projects: projects,
cocoapods: cocoapods,
packages: packages,
precompiled: precompiled,
targets: targets)
targets: targets
)
}
/// Returns a copy of the graph with the given targets.
/// - Parameter targets: Targets to be set to the copy.
/// - Returns: New graph with the given targets.
public func with(targets: [AbsolutePath: [TargetNode]]) -> Graph {
Graph(name: name,
Graph(
name: name,
entryPath: entryPath,
entryNodes: entryNodes,
workspace: workspace,
projects: projects,
cocoapods: cocoapods,
packages: packages,
precompiled: precompiled,
targets: targets)
targets: targets
)
}
/// Returns a copy of the graph with a given workspace.
/// - Parameter workspace: Workspace to be set to the copy.
/// - Returns: New graph with a given workspace.
public func with(workspace: Workspace) -> Graph {
Graph(
name: name,
entryPath: entryPath,
entryNodes: entryNodes,
workspace: workspace,
projects: projects,
cocoapods: cocoapods,
packages: packages,
precompiled: precompiled,
targets: targets
)
}
public func forEach(closure: (GraphNode) -> Void) {
@ -587,8 +651,8 @@ public class Graph: Encodable, Equatable {
stack.push(child)
}
} else if let frameworkNode = node as? FrameworkNode {
for child in frameworkNode.dependencies where !visited.contains(child) {
stack.push(child)
for child in frameworkNode.dependencies where !visited.contains(child.node) {
stack.push(child.node)
}
}
}
@ -600,11 +664,6 @@ public class Graph: Encodable, Equatable {
.product(target: targetNode.target.name, productName: targetNode.target.productNameWithExtension)
}
fileprivate func hostApplication(for targetNode: TargetNode) -> TargetNode? {
targetDependencies(path: targetNode.path, name: targetNode.name)
.first(where: { $0.target.product == .app })
}
fileprivate func isStaticLibrary(targetNode: TargetNode) -> Bool {
targetNode.target.product.isStatic
}

View File

@ -9,7 +9,7 @@ public protocol GraphLoading: AnyObject {
/// Loads the graph for the workspace in the given directory.
/// - Parameter path: Path to the directory that contains the workspace.
func loadWorkspace(path: AbsolutePath) throws -> (Graph, Workspace)
func loadWorkspace(path: AbsolutePath) throws -> Graph
/// Loads the configuration.
///
@ -64,15 +64,19 @@ public class GraphLoader: GraphLoading {
let entryNodes: [GraphNode] = try project.targets.map { target in
try self.loadTarget(name: target.name, path: path, graphLoaderCache: graphLoaderCache, graphCircularDetector: graphCircularDetector)
}
let workspace = Workspace(path: project.path, name: project.name, projects: [project.path])
let graph = Graph(name: project.name,
let graph = Graph(
name: project.name,
entryPath: path,
cache: graphLoaderCache,
entryNodes: entryNodes)
entryNodes: entryNodes,
workspace: workspace
)
return (graph, project)
}
public func loadWorkspace(path: AbsolutePath) throws -> (Graph, Workspace) {
public func loadWorkspace(path: AbsolutePath) throws -> Graph {
let graphLoaderCache = GraphLoaderCache()
let graphCircularDetector = GraphCircularDetector()
let workspace = try modelLoader.loadWorkspace(at: path)
@ -92,11 +96,14 @@ public class GraphLoader: GraphLoading {
}
}
let graph = Graph(name: workspace.name,
let graph = Graph(
name: workspace.name,
entryPath: path,
cache: graphLoaderCache,
entryNodes: entryNodes)
return (graph, workspace)
entryNodes: entryNodes,
workspace: workspace
)
return graph
}
public func loadConfig(path: AbsolutePath) throws -> Config {

View File

@ -34,4 +34,8 @@ public final class GraphTraverser: GraphTraversing {
public func directStaticDependencies(path: AbsolutePath, name: String) -> [GraphDependencyReference] {
graph.staticDependencies(path: path, name: name)
}
public func appClipsDependency(path: AbsolutePath, name: String) -> Target? {
graph.appClipsDependency(path: path, name: name).map { $0.target }
}
}

View File

@ -24,10 +24,16 @@ public class TargetNodeGraphMapper: GraphMapping {
map(node: $0, mappedCache: &mappedCache, cache: cache)
}
return (Graph(name: graph.name,
return (
Graph(
name: graph.name,
entryPath: graph.entryPath,
cache: cache,
entryNodes: updatedNodes), [])
entryNodes: updatedNodes,
workspace: graph.workspace
),
[]
)
}
// MARK: - Private

View File

@ -0,0 +1,51 @@
import Foundation
public struct WorkspaceWithProjects {
public var workspace: Workspace
public var projects: [Project]
public init(workspace: Workspace, projects: [Project]) {
self.workspace = workspace
self.projects = projects
}
}
public protocol WorkspaceMapping {
func map(workspace: WorkspaceWithProjects) throws -> (WorkspaceWithProjects, [SideEffectDescriptor])
}
public final class SequentialWorkspaceMapper: WorkspaceMapping {
let mappers: [WorkspaceMapping]
public init(mappers: [WorkspaceMapping]) {
self.mappers = mappers
}
public func map(workspace: WorkspaceWithProjects) throws -> (WorkspaceWithProjects, [SideEffectDescriptor]) {
var results = (workspace: workspace, sideEffects: [SideEffectDescriptor]())
results = try mappers.reduce(into: results) { results, mapper in
let (updatedWorkspace, sideEffects) = try mapper.map(workspace: results.workspace)
results.workspace = updatedWorkspace
results.sideEffects.append(contentsOf: sideEffects)
}
return results
}
}
public final class ProjectWorkspaceMapper: WorkspaceMapping {
private let mapper: ProjectMapping
public init(mapper: ProjectMapping) {
self.mapper = mapper
}
public func map(workspace: WorkspaceWithProjects) throws -> (WorkspaceWithProjects, [SideEffectDescriptor]) {
var results = (projects: [Project](), sideEffects: [SideEffectDescriptor]())
results = try workspace.projects.reduce(into: results) { results, project in
let (updatedProject, sideEffects) = try mapper.map(project: project)
results.projects.append(updatedProject)
results.sideEffects.append(contentsOf: sideEffects)
}
let updatedWorkspace = WorkspaceWithProjects(workspace: workspace.workspace,
projects: results.projects)
return (updatedWorkspace, results.sideEffects)
}
}

View File

@ -15,9 +15,6 @@ public class FrameworkNode: PrecompiledNode {
/// The architectures supported by the binary.
public let architectures: [BinaryArchitecture]
/// Framework dependencies.
public let dependencies: [FrameworkNode]
/// Returns the type of product.
public var product: Product {
if linking == .static {
@ -42,14 +39,13 @@ public class FrameworkNode: PrecompiledNode {
bcsymbolmapPaths: [AbsolutePath],
linking: BinaryLinking,
architectures: [BinaryArchitecture] = [],
dependencies: [FrameworkNode] = [])
dependencies: [Dependency] = [])
{
self.dsymPath = dsymPath
self.bcsymbolmapPaths = bcsymbolmapPaths
self.linking = linking
self.architectures = architectures
self.dependencies = dependencies
super.init(path: path)
super.init(path: path, dependencies: dependencies)
}
override public func encode(to encoder: Encoder) throws {

View File

@ -2,7 +2,7 @@ import Foundation
import TSCBasic
import TuistSupport
public class GraphNode: Equatable, Hashable, Encodable, CustomStringConvertible {
public class GraphNode: Equatable, Hashable, Encodable, CustomStringConvertible, CustomDebugStringConvertible {
// MARK: - Attributes
/// The path to the node.
@ -14,6 +14,9 @@ public class GraphNode: Equatable, Hashable, Encodable, CustomStringConvertible
/// The description of the node.
public var description: String { name }
/// The debug description of the node.
public var debugDescription: String { name }
// MARK: - Init
public init(path: AbsolutePath, name: String) {

View File

@ -3,11 +3,37 @@ import TSCBasic
import TuistSupport
public class PrecompiledNode: GraphNode {
public init(path: AbsolutePath) {
/// It represents a dependency of a precompiled node, which can be either a framework, or another .xcframework.
public enum Dependency: Equatable, Hashable {
case framework(FrameworkNode)
case xcframework(XCFrameworkNode)
/// Path to the dependency.
public var path: AbsolutePath {
switch self {
case let .framework(framework): return framework.path
case let .xcframework(xcframework): return xcframework.path
}
}
/// Returns the node that represents the dependency.
public var node: PrecompiledNode {
switch self {
case let .framework(framework): return framework
case let .xcframework(xcframework): return xcframework
}
}
}
/// List of other precompiled artifacts this precompiled node depends on.
public private(set) var dependencies: [Dependency]
public init(path: AbsolutePath, dependencies: [Dependency] = []) {
/// Returns the name of the precompiled node removing the extension
/// Alamofire.framework -> Alamofire
/// libAlamofire.a -> libAlamofire
let name = String(path.components.last!.split(separator: ".").first!)
self.dependencies = dependencies
super.init(path: path, name: name)
}
@ -29,4 +55,25 @@ public class PrecompiledNode: GraphNode {
case product
case type
}
/// Adds a new dependency to the xcframework node.
/// - Parameter dependency: Dependency to be added.
public func add(dependency: Dependency) {
dependencies.append(dependency)
}
// MARK: - CustomDebugStringConvertible
override public var debugDescription: String {
if dependencies.isEmpty {
return name
}
var dependenciesDescriptions: [String] = []
let uniqueDependencies = Set<Dependency>(dependencies)
uniqueDependencies.forEach { dependency in
dependenciesDescriptions.append(dependency.node.description)
}
return "\(name) --> [\(dependenciesDescriptions.joined(separator: ", "))]"
}
}

View File

@ -51,6 +51,14 @@ public class SDKNode: GraphNode {
try! SDKNode(name: "XCTest.framework", platform: platform, status: status, source: .system)
}
/// Creates an instace of SDKNode that represents the AppClip framework.
/// - Parameters:
/// - status: SDK status
/// - Returns: Initialized SDK node.
public static func appClip(status: SDKStatus) throws -> SDKNode {
try SDKNode(name: "AppClip.framework", platform: .iOS, status: status, source: .system)
}
static func path(name: String, platform: Platform, source _: SDKSource, type: Type) throws -> AbsolutePath {
let sdkRootPath: AbsolutePath
if name == SDKNode.xctestFrameworkName {

View File

@ -9,6 +9,9 @@ public class TargetNode: GraphNode {
public let target: Target
public var dependencies: [GraphNode]
/// When true it indicates that the target should be stripped from the graph when tree-shaking the project.
public var prune: Bool = false
enum CodingKeys: String, CodingKey {
case path
case name
@ -23,11 +26,13 @@ public class TargetNode: GraphNode {
public init(project: Project,
target: Target,
dependencies: [GraphNode])
dependencies: [GraphNode],
prune: Bool = false)
{
self.project = project
self.target = target
self.dependencies = dependencies
self.prune = prune
super.init(path: project.path, name: target.name)
}

View File

@ -3,28 +3,6 @@ import TSCBasic
import TuistSupport
public class XCFrameworkNode: PrecompiledNode {
/// It represents a dependency of an .xcframework which can be either a framework, or another .xcframework.
public enum Dependency: Equatable {
case framework(FrameworkNode)
case xcframework(XCFrameworkNode)
/// Path to the dependency.
public var path: AbsolutePath {
switch self {
case let .framework(framework): return framework.path
case let .xcframework(xcframework): return xcframework.path
}
}
/// Returns the node that represents the dependency.
public var node: PrecompiledNode {
switch self {
case let .framework(framework): return framework
case let .xcframework(xcframework): return xcframework
}
}
}
/// Coding keys.
enum XCFrameworkNodeCodingKeys: String, CodingKey {
case linking
@ -43,9 +21,6 @@ public class XCFrameworkNode: PrecompiledNode {
/// Returns the type of linking
public let linking: BinaryLinking
/// List of other .xcframeworks this xcframework depends on.
public private(set) var dependencies: [Dependency]
/// Path to the binary.
override public var binaryPath: AbsolutePath { primaryBinaryPath }
@ -65,8 +40,7 @@ public class XCFrameworkNode: PrecompiledNode {
self.infoPlist = infoPlist
self.linking = linking
self.primaryBinaryPath = primaryBinaryPath
self.dependencies = dependencies
super.init(path: path)
super.init(path: path, dependencies: dependencies)
}
override public func encode(to encoder: Encoder) throws {
@ -77,10 +51,4 @@ public class XCFrameworkNode: PrecompiledNode {
try container.encode("xcframework", forKey: .type)
try container.encode(infoPlist, forKey: .infoPlist)
}
/// Adds a new dependency to the xcframework node.
/// - Parameter dependency: Dependency to be added.
public func add(dependency: Dependency) {
dependencies.append(dependency)
}
}

View File

@ -41,4 +41,10 @@ public protocol GraphTraversing {
/// - path: Path to the directory where the project that defines the target is located.
/// - name: Name of the target.
func directStaticDependencies(path: AbsolutePath, name: String) -> [GraphDependencyReference]
/// Given a project directory and a target name, it returns an appClips dependency.
/// - Parameters:
/// - path: Path to the directory that contains the project.
/// - name: Target name.
func appClipsDependency(path: AbsolutePath, name: String) -> Target?
}

View File

@ -2,7 +2,7 @@ import Foundation
import TSCBasic
import TuistSupport
protocol FrameworkMetadataProviding: PrecompiledMetadataProviding {
public protocol FrameworkMetadataProviding: PrecompiledMetadataProviding {
/// Given the path to a framework, it returns the path to its dSYMs if they exist
/// in the same framework directory.
/// - Parameter frameworkPath: Path to the .framework directory.
@ -18,14 +18,14 @@ protocol FrameworkMetadataProviding: PrecompiledMetadataProviding {
func product(frameworkPath: AbsolutePath) throws -> Product
}
final class FrameworkMetadataProvider: PrecompiledMetadataProvider, FrameworkMetadataProviding {
func dsymPath(frameworkPath: AbsolutePath) -> AbsolutePath? {
public final class FrameworkMetadataProvider: PrecompiledMetadataProvider, FrameworkMetadataProviding {
public func dsymPath(frameworkPath: AbsolutePath) -> AbsolutePath? {
let path = AbsolutePath("\(frameworkPath.pathString).dSYM")
if FileHandler.shared.exists(path) { return path }
return nil
}
func bcsymbolmapPaths(frameworkPath: AbsolutePath) throws -> [AbsolutePath] {
public func bcsymbolmapPaths(frameworkPath: AbsolutePath) throws -> [AbsolutePath] {
let binaryPath = FrameworkNode.binaryPath(frameworkPath: frameworkPath)
let uuids = try self.uuids(binaryPath: binaryPath)
return uuids
@ -34,7 +34,7 @@ final class FrameworkMetadataProvider: PrecompiledMetadataProvider, FrameworkMet
.sorted()
}
func product(frameworkPath: AbsolutePath) throws -> Product {
public func product(frameworkPath: AbsolutePath) throws -> Product {
let binaryPath = FrameworkNode.binaryPath(frameworkPath: frameworkPath)
switch try linking(binaryPath: binaryPath) {
case .dynamic:

View File

@ -31,7 +31,7 @@ enum PrecompiledMetadataProviderError: FatalError, Equatable {
}
}
protocol PrecompiledMetadataProviding {
public protocol PrecompiledMetadataProviding {
/// It returns the supported architectures of the binary at the given path.
/// - Parameter binaryPath: Binary path.
func architectures(binaryPath: AbsolutePath) throws -> [BinaryArchitecture]
@ -46,10 +46,10 @@ protocol PrecompiledMetadataProviding {
func uuids(binaryPath: AbsolutePath) throws -> Set<UUID>
}
class PrecompiledMetadataProvider: PrecompiledMetadataProviding {
public class PrecompiledMetadataProvider: PrecompiledMetadataProviding {
public init() {}
func architectures(binaryPath: AbsolutePath) throws -> [BinaryArchitecture] {
public func architectures(binaryPath: AbsolutePath) throws -> [BinaryArchitecture] {
let result = try System.shared.capture("/usr/bin/lipo", "-info", binaryPath.pathString).spm_chuzzle() ?? ""
let regexes = [
// Non-fat file: path is architecture: x86_64
@ -70,12 +70,12 @@ class PrecompiledMetadataProvider: PrecompiledMetadataProviding {
return architectures
}
func linking(binaryPath: AbsolutePath) throws -> BinaryLinking {
public func linking(binaryPath: AbsolutePath) throws -> BinaryLinking {
let result = try System.shared.capture("/usr/bin/file", binaryPath.pathString).spm_chuzzle() ?? ""
return result.contains("dynamically linked") ? .dynamic : .static
}
func uuids(binaryPath: AbsolutePath) throws -> Set<UUID> {
public func uuids(binaryPath: AbsolutePath) throws -> Set<UUID> {
let output = try System.shared.capture(["/usr/bin/xcrun", "dwarfdump", "--uuid", binaryPath.pathString])
// UUIDs are letters, decimals, or hyphens.
var uuidCharacterSet = CharacterSet()

View File

@ -13,3 +13,11 @@ public enum BinaryArchitecture: String, Codable {
public enum BinaryLinking: String, Codable {
case `static`, dynamic
}
public extension Sequence where Element == BinaryArchitecture {
/// Returns true if all the architectures are only for simulator.
var onlySimulator: Bool {
let simulatorArchitectures: [BinaryArchitecture] = [.x8664, .i386]
return allSatisfy { simulatorArchitectures.contains($0) }
}
}

View File

@ -10,8 +10,10 @@ public struct Config: Equatable, Hashable {
public enum GenerationOption: Hashable, Equatable {
case xcodeProjectName(String)
case organizationName(String)
case developmentRegion(String)
case disableAutogeneratedSchemes
case disableSynthesizedResourceAccessors
case disableShowEnvironmentVarsInScriptPhases
}
/// Generation options.
@ -23,9 +25,12 @@ public struct Config: Equatable, Hashable {
/// Cloud configuration.
public let cloud: Cloud?
/// The path of the config file.
public let path: AbsolutePath?
/// Returns the default Tuist configuration.
public static var `default`: Config {
Config(compatibleXcodeVersions: .all, cloud: nil, generationOptions: [])
Config(compatibleXcodeVersions: .all, cloud: nil, generationOptions: [], path: nil)
}
/// Initializes the tuist cofiguration.
@ -34,13 +39,16 @@ public struct Config: Equatable, Hashable {
/// - compatibleXcodeVersions: List of Xcode versions the project or set of projects is compatible with.
/// - cloud: Cloud configuration.
/// - generationOptions: Generation options.
/// - path: The path of the config file.
public init(compatibleXcodeVersions: CompatibleXcodeVersions,
cloud: Cloud?,
generationOptions: [GenerationOption])
generationOptions: [GenerationOption],
path: AbsolutePath?)
{
self.compatibleXcodeVersions = compatibleXcodeVersions
self.cloud = cloud
self.generationOptions = generationOptions
self.path = path
}
// MARK: - Hashable

View File

@ -7,15 +7,18 @@ public struct ExecutionAction: Equatable {
public let title: String
public let scriptText: String
public let target: TargetReference?
public let showEnvVarsInLog: Bool
// MARK: - Init
public init(title: String,
scriptText: String,
target: TargetReference?)
target: TargetReference?,
showEnvVarsInLog: Bool = true)
{
self.title = title
self.scriptText = scriptText
self.target = target
self.showEnvVarsInLog = showEnvVarsInLog
}
}

View File

@ -19,6 +19,7 @@ public enum Product: String, CustomStringConvertible, CaseIterable, Encodable {
// case messagesApplication = "messages_application"
case messagesExtension = "messages_extension"
case stickerPackExtension = "sticker_pack_extension"
case appClip
public var caseValue: String {
switch self {
@ -56,6 +57,8 @@ public enum Product: String, CustomStringConvertible, CaseIterable, Encodable {
return "messagesExtension"
case .stickerPackExtension:
return "stickerPackExtension"
case .appClip:
return "appClip"
}
}
@ -95,13 +98,15 @@ public enum Product: String, CustomStringConvertible, CaseIterable, Encodable {
return "iMessage extension"
case .stickerPackExtension:
return "sticker pack extension"
case .appClip:
return "appClip"
}
}
/// Returns true if the target can be ran.
public var runnable: Bool {
switch self {
case .app:
case .app, .appClip:
return true
default:
return false
@ -126,6 +131,7 @@ public enum Product: String, CustomStringConvertible, CaseIterable, Encodable {
base.append(.stickerPackExtension)
// base.append(.messagesApplication)
base.append(.messagesExtension)
base.append(.appClip)
}
if platform == .tvOS {
@ -195,6 +201,12 @@ public enum Product: String, CustomStringConvertible, CaseIterable, Encodable {
return .messagesExtension
case .stickerPackExtension:
return .stickerPack
case .appClip:
return .onDemandInstallCapableApplication
}
}
public func canHostTests() -> Bool {
[.app, .appClip].contains(self)
}
}

View File

@ -9,6 +9,7 @@ public struct Project: Hashable, Equatable, CustomStringConvertible, CustomDebug
lhs.xcodeProjPath == rhs.xcodeProjPath &&
lhs.name == rhs.name &&
lhs.organizationName == rhs.organizationName &&
lhs.developmentRegion == rhs.developmentRegion &&
lhs.targets == rhs.targets &&
lhs.packages == rhs.packages &&
lhs.schemes == rhs.schemes &&
@ -34,6 +35,9 @@ public struct Project: Hashable, Equatable, CustomStringConvertible, CustomDebug
/// Organization name.
public var organizationName: String?
/// Development region code e.g. `en`.
public var developmentRegion: String?
/// Project targets.
public var targets: [Target]
@ -62,6 +66,7 @@ public struct Project: Hashable, Equatable, CustomStringConvertible, CustomDebug
/// - xcodeProjPath: Path to the Xcode project that will be generated.
/// - name: Project name.
/// - organizationName: Organization name.
/// - developmentRegion: Development region.
/// - settings: The settings to apply at the project level
/// - filesGroup: The root group to place project files within
/// - targets: The project targets
@ -72,6 +77,7 @@ public struct Project: Hashable, Equatable, CustomStringConvertible, CustomDebug
xcodeProjPath: AbsolutePath,
name: String,
organizationName: String?,
developmentRegion: String?,
settings: Settings,
filesGroup: ProjectGroup,
targets: [Target],
@ -84,6 +90,7 @@ public struct Project: Hashable, Equatable, CustomStringConvertible, CustomDebug
self.xcodeProjPath = xcodeProjPath
self.name = name
self.organizationName = organizationName
self.developmentRegion = developmentRegion
self.targets = targets
self.packages = packages
self.schemes = schemes
@ -155,6 +162,7 @@ public struct Project: Hashable, Equatable, CustomStringConvertible, CustomDebug
xcodeProjPath: xcodeProjPath,
name: name,
organizationName: organizationName,
developmentRegion: developmentRegion,
settings: settings,
filesGroup: filesGroup,
targets: targets,
@ -171,6 +179,7 @@ public struct Project: Hashable, Equatable, CustomStringConvertible, CustomDebug
xcodeProjPath: xcodeProjPath,
name: name,
organizationName: organizationName,
developmentRegion: developmentRegion,
settings: settings,
filesGroup: filesGroup,
targets: targets,

View File

@ -47,6 +47,7 @@ public struct Target: Equatable, Hashable, Comparable {
public var environment: [String: String]
public var launchArguments: [String: Bool]
public var filesGroup: ProjectGroup
public var scripts: [TargetScript]
// MARK: - Init
@ -67,7 +68,8 @@ public struct Target: Equatable, Hashable, Comparable {
environment: [String: String] = [:],
launchArguments: [String: Bool] = [:],
filesGroup: ProjectGroup,
dependencies: [Dependency] = [])
dependencies: [Dependency] = [],
scripts: [TargetScript] = [])
{
self.name = name
self.product = product
@ -87,6 +89,7 @@ public struct Target: Equatable, Hashable, Comparable {
self.launchArguments = launchArguments
self.filesGroup = filesGroup
self.dependencies = dependencies
self.scripts = scripts
}
/// Target can be included in the link phase of other targets
@ -116,6 +119,17 @@ public struct Target: Equatable, Hashable, Comparable {
].contains(product)
}
/// It returns the name of the variable that should be used to create an empty file
/// in the $BUILT_PRODUCTS_DIR directory that is used after builds to reliably locate the
/// directories where the products have been exported into.
public var targetLocatorBuildPhaseVariable: String {
let upperCasedSnakeCasedProductName = productName
.camelCaseToSnakeCase()
.components(separatedBy: .whitespaces).joined(separator: "_")
.uppercased()
return "\(upperCasedSnakeCasedProductName)_LOCATE_HASH"
}
/// Returns the product name including the extension.
public var productNameWithExtension: String {
switch product {
@ -126,6 +140,16 @@ public struct Target: Equatable, Hashable, Comparable {
}
}
/// Returns true if the target supports having a headers build phase..
public var shouldIncludeHeadersBuildPhase: Bool {
switch product {
case .framework, .staticFramework, .staticLibrary, .dynamicLibrary:
return true
default:
return false
}
}
/// Returns true if the target supports having sources.
public var supportsSources: Bool {
switch (platform, product) {
@ -146,6 +170,14 @@ public struct Target: Equatable, Hashable, Comparable {
}
}
/// Returns true if the target is an AppClip
public var isAppClip: Bool {
if case .appClip = product {
return true
}
return false
}
/// Returns true if the file at the given path is a resource.
/// - Parameter path: Path to the file to be checked.
public static func isResource(path: AbsolutePath) -> Bool {
@ -263,8 +295,8 @@ extension Sequence where Element == Target {
filter { $0.product.testsBundle }
}
/// Filters and returns only the targets that are apps.
/// Filters and returns only the targets that are apps and app clips.
var apps: [Target] {
filter { $0.product == .app }
filter { $0.product == .app || $0.product == .appClip }
}
}

View File

@ -40,6 +40,12 @@ public struct TargetAction: Equatable {
/// List of output filelist paths
public let outputFileListPaths: [AbsolutePath]
/// Show environment variables in the logs
public var showEnvVarsInLog: Bool
/// Whether to skip running this script in incremental builds, if nothing has changed
public let basedOnDependencyAnalysis: Bool?
/// Initializes a new target action with its attributes.
///
/// - Parameters:
@ -52,6 +58,8 @@ public struct TargetAction: Equatable {
/// - inputFileListPaths: List of input filelist paths
/// - outputPaths: List of output file paths
/// - outputFileListPaths: List of output filelist paths
/// - showEnvVarsInLog: Show environment variables in the logs
/// - basedOnDependencyAnalysis: Whether to skip running this script in incremental builds
public init(name: String,
order: Order,
tool: String? = nil,
@ -60,7 +68,9 @@ public struct TargetAction: Equatable {
inputPaths: [AbsolutePath] = [],
inputFileListPaths: [AbsolutePath] = [],
outputPaths: [AbsolutePath] = [],
outputFileListPaths: [AbsolutePath] = [])
outputFileListPaths: [AbsolutePath] = [],
showEnvVarsInLog: Bool = true,
basedOnDependencyAnalysis: Bool? = nil)
{
self.name = name
self.order = order
@ -71,6 +81,8 @@ public struct TargetAction: Equatable {
self.inputFileListPaths = inputFileListPaths
self.outputPaths = outputPaths
self.outputFileListPaths = outputFileListPaths
self.showEnvVarsInLog = showEnvVarsInLog
self.basedOnDependencyAnalysis = basedOnDependencyAnalysis
}
/// Returns the shell script that should be used in the target build phase.

View File

@ -0,0 +1,34 @@
import Foundation
import TSCBasic
import TuistSupport
/// It represents a target build phase.
public struct TargetScript: Equatable {
/// The name of the build phase.
public let name: String
/// Script.
public let script: String
/// Whether we want the build phase to show the environment variables in the logs.
public let showEnvVarsInLog: Bool
/// Whether the script should be hashed for caching purposes.
public let hashable: Bool
/// Initializes the target script.
/// - Parameter name: The name of the build phase.
/// - Parameter script: Script.
/// - Parameter showEnvVarsInLog: Whether we want the build phase to show the environment variables in the logs.
/// - Parameter hashable: Whether the script should be hashed for caching purposes.
public init(name: String,
script: String,
showEnvVarsInLog: Bool,
hashable: Bool)
{
self.name = name
self.script = script
self.showEnvVarsInLog = showEnvVarsInLog
self.hashable = hashable
}
}

View File

@ -1,7 +1,7 @@
import Foundation
import TSCBasic
public struct TestableTarget: Equatable {
public struct TestableTarget: Equatable, Hashable {
public let target: TargetReference
public let isSkipped: Bool
public let isParallelizable: Bool

View File

@ -5,11 +5,11 @@ import TuistSupport
public struct Workspace: Equatable {
// MARK: - Attributes
public let path: AbsolutePath
public let name: String
public let projects: [AbsolutePath]
public let schemes: [Scheme]
public let additionalFiles: [FileElement]
public var path: AbsolutePath
public var name: String
public var projects: [AbsolutePath]
public var schemes: [Scheme]
public var additionalFiles: [FileElement]
// MARK: - Init
@ -23,6 +23,12 @@ public struct Workspace: Equatable {
}
extension Workspace {
public func with(name: String) -> Workspace {
var copy = self
copy.name = name
return copy
}
public func adding(files: [AbsolutePath]) -> Workspace {
Workspace(path: path,
name: name,

View File

@ -28,17 +28,17 @@ public protocol FrameworkNodeLoading {
func load(path: AbsolutePath) throws -> FrameworkNode
}
final class FrameworkNodeLoader: FrameworkNodeLoading {
public final class FrameworkNodeLoader: FrameworkNodeLoading {
/// Framework metadata provider.
fileprivate let frameworkMetadataProvider: FrameworkMetadataProviding
/// Initializes the loader with its attributes.
/// - Parameter frameworkMetadataProvider: Framework metadata provider.
init(frameworkMetadataProvider: FrameworkMetadataProviding = FrameworkMetadataProvider()) {
public init(frameworkMetadataProvider: FrameworkMetadataProviding = FrameworkMetadataProvider()) {
self.frameworkMetadataProvider = frameworkMetadataProvider
}
func load(path: AbsolutePath) throws -> FrameworkNode {
public func load(path: AbsolutePath) throws -> FrameworkNode {
guard FileHandler.shared.exists(path) else {
throw FrameworkNodeLoaderError.frameworkNotFound(path)
}

View File

@ -1,38 +1,62 @@
import Foundation
import RxSwift
import struct TSCUtility.Version
import TuistSupport
protocol SimulatorControlling {
public protocol SimulatorControlling {
/// Returns the list of simulator devices that are available in the system.
func devices() -> Single<[SimulatorDevice]>
/// Returns the list of simulator runtimes that are available in the system.
func runtimes() -> Single<[SimulatorRuntime]>
/// Returns the list of simulator devices and runtimes.
/// - Parameters:
/// - platform: Optionally filter by platform
/// - deviceName: Optionally filter by device name
/// - Returns: the list of simulator devices and runtimes.
func devicesAndRuntimes() -> Single<[SimulatorDeviceAndRuntime]>
/// Finds first available device defined by given parameters
/// - Parameters:
/// - platform: Given platform
/// - version: Specific version, ignored if nil
/// - minVersion: Minimum version of the OS
/// - deviceName: Specific device name (eg. iPhone X)
func findAvailableDevice(
platform: Platform,
version: Version?,
minVersion: Version?,
deviceName: String?
) -> Single<SimulatorDevice>
}
enum SimulatorControllerError: FatalError {
public enum SimulatorControllerError: FatalError {
case simctlError(String)
case deviceNotFound(Platform, Version?, String?, [SimulatorDeviceAndRuntime])
var type: ErrorType {
public var type: ErrorType {
switch self {
case .simctlError: return .abort
case .simctlError, .deviceNotFound:
return .abort
}
}
var description: String {
public var description: String {
switch self {
case let .simctlError(output): return output
case let .simctlError(output):
return output
case let .deviceNotFound(platform, version, deviceName, devices):
return "Could not find a suitable device for \(platform.caseValue)\(version.map { " \($0)" } ?? "")\(deviceName.map { ", device name \($0)" } ?? ""). Did find \(devices.map { "\($0.device.name) (\($0.runtime.description))" }.joined(separator: ", "))"
}
}
}
final class SimulatorController: SimulatorControlling {
private let jsonDecoder: JSONDecoder = JSONDecoder()
public final class SimulatorController: SimulatorControlling {
private let jsonDecoder = JSONDecoder()
func devices() -> Single<[SimulatorDevice]> {
public init() {}
public func devices() -> Single<[SimulatorDevice]> {
System.shared.observable(["/usr/bin/xcrun", "simctl", "list", "devices", "--json"])
.mapToString()
.collectOutput()
@ -63,9 +87,8 @@ final class SimulatorController: SimulatorControlling {
}
}
func runtimes() -> Single<[SimulatorRuntime]> {
public func runtimes() -> Single<[SimulatorRuntime]> {
System.shared.observable(["/usr/bin/xcrun", "simctl", "list", "runtimes", "--json"])
.debug()
.mapToString()
.collectOutput()
.asSingle()
@ -88,7 +111,7 @@ final class SimulatorController: SimulatorControlling {
}
}
func devicesAndRuntimes() -> Single<[SimulatorDeviceAndRuntime]> {
public func devicesAndRuntimes() -> Single<[SimulatorDeviceAndRuntime]> {
runtimes()
.flatMap { (runtimes) -> Single<([SimulatorDevice], [SimulatorRuntime])> in
self.devices().map { ($0, runtimes) }
@ -100,4 +123,36 @@ final class SimulatorController: SimulatorControlling {
}
}
}
public func findAvailableDevice(
platform: Platform,
version: Version?,
minVersion: Version?,
deviceName: String?
) -> Single<SimulatorDevice> {
devicesAndRuntimes()
.flatMap { devicesAndRuntimes in
let availableDevices = devicesAndRuntimes
.filter { simulatorDeviceAndRuntime in
let nameComponents = simulatorDeviceAndRuntime.runtime.name.components(separatedBy: " ")
guard nameComponents.first == platform.caseValue else { return false }
let deviceVersion = nameComponents.last?.version()
if let version = version {
guard deviceVersion == version else { return false }
} else if let minVersion = minVersion, let deviceVersion = deviceVersion {
guard deviceVersion >= minVersion else { return false }
}
if let deviceName = deviceName {
guard simulatorDeviceAndRuntime.device.name == deviceName else { return false }
}
return true
}
.map(\.device)
guard
let device = availableDevices.first(where: { !$0.isShutdown }) ?? availableDevices.first
else { return .error(SimulatorControllerError.deviceNotFound(platform, version, deviceName, devicesAndRuntimes)) }
return .just(device)
}
}
}

View File

@ -2,44 +2,45 @@ import Foundation
import TSCBasic
/// It represents a simulator device. Devices are obtained using Xcode's CLI simctl
struct SimulatorDevice: Decodable, Hashable, CustomStringConvertible {
public struct SimulatorDevice: Decodable, Hashable, CustomStringConvertible {
/// Device data path.
let dataPath: AbsolutePath
public let dataPath: AbsolutePath
/// Device log path.
let logPath: AbsolutePath
public let logPath: AbsolutePath
/// Device unique identifier (3A8C9673-C1FD-4E33-8EFA-AEEBF43161CC)
let udid: String
public let udid: String
/// Whether the device is available or not.
let isAvailable: Bool
public let isAvailable: Bool
/// Device type identifier (e.g. com.apple.CoreSimulator.SimDeviceType.iPad-Air--3rd-generation-)
let deviceTypeIdentifier: String?
public let deviceTypeIdentifier: String?
/// Device state (e.g. Shutdown)
let state: String
public let state: String
/// Returns true if the device is shutdown.
var isShutdown: Bool {
public var isShutdown: Bool {
state == "Shutdown"
}
/// Device name (e.g. iPad Air (3rd generation))
let name: String
public let name: String
/// If the device is not available, this provides a description of the error.
let availabilityError: String?
public let availabilityError: String?
/// Device runtime identifier (e.g. com.apple.CoreSimulator.SimRuntime.iOS-13-5)
let runtimeIdentifier: String
public let runtimeIdentifier: String
var description: String {
public var description: String {
name
}
public init(dataPath: AbsolutePath,
public init(
dataPath: AbsolutePath,
logPath: AbsolutePath,
udid: String,
isAvailable: Bool,
@ -47,8 +48,8 @@ struct SimulatorDevice: Decodable, Hashable, CustomStringConvertible {
state: String,
name: String,
availabilityError: String?,
runtimeIdentifier: String)
{
runtimeIdentifier: String
) {
self.dataPath = dataPath
self.logPath = logPath
self.udid = udid

View File

@ -0,0 +1,9 @@
import Foundation
public struct SimulatorDeviceAndRuntime: Hashable {
/// Device
public let device: SimulatorDevice
/// Device's runtime.
public let runtime: SimulatorRuntime
}

View File

@ -3,36 +3,37 @@ import TSCBasic
/// It represents a runtime that is available in the system. The list of available runtimes is obtained
/// using Xcode's simctl cli tool.
struct SimulatorRuntime: Decodable, Hashable, CustomStringConvertible {
public struct SimulatorRuntime: Decodable, Hashable, CustomStringConvertible {
/// Runtime bundle path (e.g. /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime)
let bundlePath: AbsolutePath
public let bundlePath: AbsolutePath
/// Runtime build version (e.g. 17F61)
let buildVersion: String
public let buildVersion: String
/// Runtime root (e.g. /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes\/iOS.simruntime\Contents/Resources/RuntimeRoot)
let runtimeRoot: AbsolutePath
public let runtimeRoot: AbsolutePath
/// Runtime identifier (e.g. com.apple.CoreSimulator.SimRuntime.iOS-13-5)
let identifier: String
public let identifier: String
/// Runtime version (e.g. 13.5)
let version: SimulatorRuntimeVersion
public let version: SimulatorRuntimeVersion
// True if the runtime is available.
let isAvailable: Bool
public let isAvailable: Bool
// Name of the runtime (e.g. iOS 13.5)
let name: String
public let name: String
init(bundlePath: AbsolutePath,
public init(
bundlePath: AbsolutePath,
buildVersion: String,
runtimeRoot: AbsolutePath,
identifier: String,
version: SimulatorRuntimeVersion,
isAvailable: Bool,
name: String)
{
name: String
) {
self.bundlePath = bundlePath
self.buildVersion = buildVersion
self.runtimeRoot = runtimeRoot
@ -52,7 +53,7 @@ struct SimulatorRuntime: Decodable, Hashable, CustomStringConvertible {
case name
}
var description: String {
public var description: String {
name
}
}

View File

@ -1,11 +1,11 @@
import Foundation
struct SimulatorRuntimeVersion: CustomStringConvertible, Hashable, ExpressibleByStringLiteral, Comparable, Decodable {
public struct SimulatorRuntimeVersion: CustomStringConvertible, Hashable, ExpressibleByStringLiteral, Comparable, Decodable {
// MARK: - Attributes
let major: Int
let minor: Int?
let patch: Int?
public let major: Int
public let minor: Int?
public let patch: Int?
// MARK: - Constructors
@ -15,7 +15,7 @@ struct SimulatorRuntimeVersion: CustomStringConvertible, Hashable, ExpressibleBy
self.patch = patch
}
init(from decoder: Decoder) throws {
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
self.init(stringLiteral: try container.decode(String.self))
}
@ -30,7 +30,7 @@ struct SimulatorRuntimeVersion: CustomStringConvertible, Hashable, ExpressibleBy
// MARK: - CustomStringConvertible
var description: String {
public var description: String {
var version = "\(major)"
if let minor = minor {
version.append(".\(minor)")
@ -47,7 +47,7 @@ struct SimulatorRuntimeVersion: CustomStringConvertible, Hashable, ExpressibleBy
// MARK: - Equatable
static func == (lhs: SimulatorRuntimeVersion, rhs: SimulatorRuntimeVersion) -> Bool {
public static func == (lhs: SimulatorRuntimeVersion, rhs: SimulatorRuntimeVersion) -> Bool {
lhs.major == rhs.major &&
lhs.minor == rhs.minor &&
lhs.patch == rhs.patch
@ -55,7 +55,7 @@ struct SimulatorRuntimeVersion: CustomStringConvertible, Hashable, ExpressibleBy
// MARK: - Comparable
static func < (lhs: SimulatorRuntimeVersion, rhs: SimulatorRuntimeVersion) -> Bool {
public static func < (lhs: SimulatorRuntimeVersion, rhs: SimulatorRuntimeVersion) -> Bool {
let lhs = lhs.flattened()
let rhs = rhs.flattened()
@ -76,7 +76,7 @@ struct SimulatorRuntimeVersion: CustomStringConvertible, Hashable, ExpressibleBy
// MARK: - ExpressibleByStringLiteral
init(stringLiteral value: String) {
public init(stringLiteral value: String) {
let components = value.split(separator: ".")
// Major

View File

@ -72,7 +72,7 @@ public struct ValueGraph: Equatable {
if let targetNode = node as? TargetNode {
targetNode.dependencies.forEach { nodeDependencies.formUnion([self.dependency(from: $0)]) }
} else if let frameworkNode = node as? FrameworkNode {
frameworkNode.dependencies.forEach { nodeDependencies.formUnion([self.dependency(from: $0)]) }
frameworkNode.dependencies.forEach { nodeDependencies.formUnion([self.dependency(from: $0.node)]) }
} else if let xcframeworkNode = node as? XCFrameworkNode {
xcframeworkNode.dependencies.forEach { nodeDependencies.formUnion([self.dependency(from: $0.node)]) }
}

View File

@ -71,6 +71,11 @@ public class ValueGraphTraverser: GraphTraversing {
.sorted()
}
public func appClipsDependency(path: AbsolutePath, name: String) -> Target? {
directTargetDependencies(path: path, name: name)
.first { $0.product == .appClip }
}
public func directStaticDependencies(path: AbsolutePath, name: String) -> [GraphDependencyReference] {
graph.dependencies[.target(name: name, path: path)]?
.compactMap { (dependency: ValueGraphDependency) -> (path: AbsolutePath, name: String)? in

View File

@ -19,6 +19,21 @@ final class MockXcodeBuildController: XcodeBuildControlling {
}
}
var testStub: ((XcodeBuildTarget, String, Bool, XcodeBuildDestination, [XcodeBuildArgument]) -> Observable<SystemEvent<XcodeBuildOutput>>)?
func test(
_ target: XcodeBuildTarget,
scheme: String,
clean: Bool,
destination: XcodeBuildDestination,
arguments: [XcodeBuildArgument]
) -> Observable<SystemEvent<XcodeBuildOutput>> {
if let testStub = testStub {
return testStub(target, scheme, clean, destination, arguments)
} else {
return Observable.error(TestError("\(String(describing: MockXcodeBuildController.self)) received an unexpected call to test"))
}
}
var archiveStub: ((XcodeBuildTarget, String, Bool, AbsolutePath, [XcodeBuildArgument]) -> Observable<SystemEvent<XcodeBuildOutput>>)?
func archive(_ target: XcodeBuildTarget,
scheme: String,

View File

@ -9,7 +9,7 @@ public extension FrameworkNode {
bcsymbolmapPaths: [AbsolutePath] = [],
linking: BinaryLinking = .dynamic,
architectures: [BinaryArchitecture] = [],
dependencies: [FrameworkNode] = []) -> FrameworkNode
dependencies: [PrecompiledNode.Dependency] = []) -> FrameworkNode
{
FrameworkNode(path: path,
dsymPath: dsymPath,

View File

@ -3,23 +3,28 @@ import TSCBasic
@testable import TuistCore
public extension Graph {
static func test(name: String = "test",
static func test(
name: String = "test",
entryPath: AbsolutePath = AbsolutePath("/test/graph"),
entryNodes: [GraphNode] = [],
workspace: Workspace = Workspace.test(path: "/test/graph"),
projects: [Project] = [],
cocoapods: [CocoaPodsNode] = [],
packages: [PackageNode] = [],
precompiled: [PrecompiledNode] = [],
targets: [AbsolutePath: [TargetNode]] = [:]) -> Graph
{
Graph(name: name,
targets: [AbsolutePath: [TargetNode]] = [:]
) -> Graph {
Graph(
name: name,
entryPath: entryPath,
entryNodes: entryNodes,
workspace: workspace,
projects: projects,
cocoapods: cocoapods,
packages: packages,
precompiled: precompiled,
targets: targets)
targets: targets
)
}
/// Creates a test dependency graph for targets within a single project

View File

@ -10,9 +10,9 @@ public final class MockGraphLoader: GraphLoading {
return try loadProjectStub?(path) ?? (Graph.test(), Project.test())
}
public var loadWorkspaceStub: ((AbsolutePath) throws -> (Graph, Workspace))?
public func loadWorkspace(path: AbsolutePath) throws -> (Graph, Workspace) {
return try loadWorkspaceStub?(path) ?? (Graph.test(), Workspace.test())
public var loadWorkspaceStub: ((AbsolutePath) throws -> (Graph))?
public func loadWorkspace(path: AbsolutePath) throws -> (Graph) {
return try loadWorkspaceStub?(path) ?? Graph.test()
}
public var loadConfigStub: ((AbsolutePath) throws -> (Config))?

View File

@ -6,10 +6,12 @@ import TSCBasic
public extension TargetNode {
static func test(project: Project = .test(),
target: Target = .test(),
dependencies: [GraphNode] = []) -> TargetNode
dependencies: [GraphNode] = [],
prune: Bool = false) -> TargetNode
{
TargetNode(project: project,
target: target,
dependencies: dependencies)
dependencies: dependencies,
prune: prune)
}
}

View File

@ -8,7 +8,7 @@ public extension XCFrameworkNode {
infoPlist: XCFrameworkInfoPlist = .test(),
primaryBinaryPath: AbsolutePath = "/MyFramework/MyFramework.xcframework/binary",
linking: BinaryLinking = .dynamic,
dependencies: [XCFrameworkNode.Dependency] = []) -> XCFrameworkNode
dependencies: [PrecompiledNode.Dependency] = []) -> XCFrameworkNode
{
XCFrameworkNode(path: path,
infoPlist: infoPlist,

View File

@ -100,4 +100,8 @@ final class MockGraphTraverser: GraphTraversing {
invokedDirectStaticDependenciesParametersList.append((path, name))
return stubbedDirectStaticDependenciesResult
}
func appClipsDependency(path _: AbsolutePath, name _: String) -> Target? {
nil
}
}

View File

@ -5,10 +5,12 @@ import TSCBasic
public extension Config {
static func test(compatibleXcodeVersions: CompatibleXcodeVersions = .all,
cloud: Cloud? = Cloud.test(),
generationOptions: [GenerationOption] = []) -> Config
generationOptions: [GenerationOption] = [],
path: AbsolutePath? = nil) -> Config
{
Config(compatibleXcodeVersions: compatibleXcodeVersions,
cloud: cloud,
generationOptions: generationOptions)
generationOptions: generationOptions,
path: path)
}
}

View File

@ -8,6 +8,7 @@ public extension Project {
xcodeProjPath: AbsolutePath = AbsolutePath("/Project/Project.xcodeproj"),
name: String = "Project",
organizationName: String? = nil,
developmentRegion: String? = nil,
settings: Settings = Settings.test(),
filesGroup: ProjectGroup = .group(name: "Project"),
targets: [Target] = [Target.test()],
@ -20,6 +21,7 @@ public extension Project {
xcodeProjPath: xcodeProjPath,
name: name,
organizationName: organizationName,
developmentRegion: developmentRegion,
settings: settings,
filesGroup: filesGroup,
targets: targets,
@ -33,6 +35,7 @@ public extension Project {
xcodeProjPath: AbsolutePath = AbsolutePath("/test/text.xcodeproj"),
name: String = "Project",
organizationName: String? = nil,
developmentRegion: String? = nil,
settings: Settings = .default,
filesGroup: ProjectGroup = .group(name: "Project"),
targets: [Target] = [],
@ -45,6 +48,7 @@ public extension Project {
xcodeProjPath: xcodeProjPath,
name: name,
organizationName: organizationName,
developmentRegion: developmentRegion,
settings: settings,
filesGroup: filesGroup,
targets: targets,

View File

@ -21,7 +21,8 @@ public extension Target {
actions: [TargetAction] = [],
environment: [String: String] = [:],
filesGroup: ProjectGroup = .group(name: "Project"),
dependencies: [Dependency] = []) -> Target
dependencies: [Dependency] = [],
scripts: [TargetScript] = []) -> Target
{
Target(name: name,
platform: platform,
@ -39,7 +40,8 @@ public extension Target {
actions: actions,
environment: environment,
filesGroup: filesGroup,
dependencies: dependencies)
dependencies: dependencies,
scripts: scripts)
}
/// Creates a bare bones Target with as little data as possible
@ -59,7 +61,8 @@ public extension Target {
actions: [TargetAction] = [],
environment: [String: String] = [:],
filesGroup: ProjectGroup = .group(name: "Project"),
dependencies: [Dependency] = []) -> Target
dependencies: [Dependency] = [],
scripts: [TargetScript] = []) -> Target
{
Target(name: name,
platform: platform,
@ -77,6 +80,7 @@ public extension Target {
actions: actions,
environment: environment,
filesGroup: filesGroup,
dependencies: dependencies)
dependencies: dependencies,
scripts: scripts)
}
}

View File

@ -0,0 +1,13 @@
import Foundation
import TSCBasic
@testable import TuistCore
public extension TargetScript {
static func test(name: String = "Test",
script: String = "",
showEnvVarsInLog: Bool = false,
hashable: Bool = false) -> TargetScript
{
TargetScript(name: name, script: script, showEnvVarsInLog: showEnvVarsInLog, hashable: hashable)
}
}

View File

@ -3,14 +3,19 @@ import TSCBasic
@testable import TuistCore
public extension Workspace {
static func test(path: AbsolutePath = AbsolutePath("/"),
static func test(
path: AbsolutePath = AbsolutePath("/"),
name: String = "test",
projects: [AbsolutePath] = [],
additionalFiles: [FileElement] = []) -> Workspace
{
Workspace(path: path,
schemes: [Scheme] = [],
additionalFiles: [FileElement] = []
) -> Workspace {
Workspace(
path: path,
name: name,
projects: projects,
additionalFiles: additionalFiles)
schemes: schemes,
additionalFiles: additionalFiles
)
}
}

View File

@ -1,8 +1,10 @@
import Foundation
import RxSwift
import struct TSCUtility.Version
import TuistCore
import TuistSupport
@testable import TuistAutomation
@testable import TuistCore
@testable import TuistSupportTesting
public final class MockSimulatorController: SimulatorControlling {
@ -43,4 +45,9 @@ public final class MockSimulatorController: SimulatorControlling {
return .error(TestError("call to non-stubbed method runtimesAndDevices"))
}
}
public var findAvailableDeviceStub: ((Platform, Version?, Version?, String?) -> Single<SimulatorDevice>)?
public func findAvailableDevice(platform: Platform, version: Version?, minVersion: Version?, deviceName: String?) -> Single<SimulatorDevice> {
findAvailableDeviceStub?(platform, version, minVersion, deviceName) ?? .just(SimulatorDevice.test())
}
}

View File

@ -1,6 +1,6 @@
import Foundation
import TSCBasic
@testable import TuistAutomation
@testable import TuistCore
extension SimulatorDevice {
static func test(dataPath: AbsolutePath = "/Library/Developer/CoreSimulator/Devices/3A8C9673-C1FD-4E33-8EFA-AEEBF43161CC/data",

View File

@ -1,6 +1,6 @@
import Foundation
import TSCBasic
@testable import TuistAutomation
@testable import TuistCore
extension SimulatorDeviceAndRuntime {
static func test(device: SimulatorDevice = .test(),

View File

@ -1,6 +1,6 @@
import Foundation
import TSCBasic
@testable import TuistAutomation
@testable import TuistCore
extension SimulatorRuntime {
// swiftlint:disable:next line_length

View File

@ -3,8 +3,6 @@ import TSCBasic
@testable import TuistCore
public final class MockRootDirectoryLocator: RootDirectoryLocating {
public init() {}
public var locateArgs: [AbsolutePath] = []
public var locateStub: AbsolutePath?

Some files were not shown because too many files have changed in this diff Show More