diff --git a/.all-contributorsrc b/.all-contributorsrc index 12d5ec2d19a..2138fa98f0b 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -1237,6 +1237,42 @@ "contributions": [ "code" ] + }, + { + "login": "dxmvsh", + "name": "Dimash", + "avatar_url": "https://avatars.githubusercontent.com/u/44325936?v=4", + "profile": "https://github.com/dxmvsh", + "contributions": [ + "code" + ] + }, + { + "login": "danibachar", + "name": "danibachar", + "avatar_url": "https://avatars.githubusercontent.com/u/6380777?v=4", + "profile": "https://github.com/danibachar", + "contributions": [ + "code" + ] + }, + { + "login": "dp221125", + "name": "한석호(MilKyo)", + "avatar_url": "https://avatars.githubusercontent.com/u/10572119?v=4", + "profile": "https://github.com/dp221125", + "contributions": [ + "code" + ] + }, + { + "login": "haifengkao", + "name": "Hai Feng Kao", + "avatar_url": "https://avatars.githubusercontent.com/u/4080524?v=4", + "profile": "https://medium.com/@haifengkaohaifengkao&usg=AOvVaw2_xG-ZLdBawBIyS7m-99RQ", + "contributions": [ + "code" + ] } ], "contributorsPerLine": 7, diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index fae50dada5b..62fc6531c89 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,2 +1,3 @@ open_collective: tuistapp github: tuist +custom: ["https://polar.sh/tuist"] diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 4b7eff7ff57..b4c0b29d1a2 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -10,7 +10,7 @@ Resolves https://github.com/tuist/tuist/issues/YYY ### Contributor checklist ✅ -- [ ] The code has been linted using run `make workspace/lint-fix` +- [ ] The code has been linted using run `mise run lint:fix` - [ ] The change is tested via unit testing or acceptance testing, or both - [ ] The title of the PR is formulated in a way that is usable as a changelog entry - [ ] In case the PR introduces changes that affect users, the documentation has been updated diff --git a/.github/workflows/changelog-configuration.json b/.github/tuist-changelog-configuration.json similarity index 100% rename from .github/workflows/changelog-configuration.json rename to .github/tuist-changelog-configuration.json diff --git a/.github/tuist-cloud-changelog-configuration.json b/.github/tuist-cloud-changelog-configuration.json new file mode 100644 index 00000000000..e1a002a9ad2 --- /dev/null +++ b/.github/tuist-cloud-changelog-configuration.json @@ -0,0 +1,17 @@ +{ + "categories": [ + { + "title": "#### Changed", + "labels": ["changelog:changed"] + }, + { + "title": "#### Added", + "labels": ["changelog:added"] + }, + { + "title": "#### Fixed", + "labels": ["changelog:fixed"] + } + ], + "pr_template": "- ${{TITLE}} by [@${{AUTHOR}}](https://github.com/${{AUTHOR}})" +} diff --git a/.github/workflows/deploy-description-docc.yml b/.github/workflows/deploy-description-docc.yml deleted file mode 100644 index 2ca6a70161c..00000000000 --- a/.github/workflows/deploy-description-docc.yml +++ /dev/null @@ -1,68 +0,0 @@ -# Build and deploy DocC to GitHub pages. Based off of Pointfree's work here: -# https://github.com/pointfreeco/swift-parsing/blob/main/.github/workflows/documentation.yml -name: Deploy Desscription Docc - -on: - release: - types: - - published - push: - branches: - - main - -concurrency: - group: description-docc-${{ github.head_ref }} - cancel-in-progress: true - -jobs: - build: - runs-on: macos-13 - steps: - - name: Checkout Package - uses: actions/checkout@v3 - with: - fetch-depth: 0 - - name: Checkout gh-pages Branch - uses: actions/checkout@v3 - with: - ref: gh-pages - path: docs-out - - name: Build documentation - run: > - rm -rf docs-out/.git; - rm -rf docs-out/main; - rm -rf docs-out/latest; - for tag in $(echo "main"; echo "latest"; git tag | tail -n +94); - do - echo "⏳ Generating documentation for "$tag" release."; - - if [ -d "docs-out/$tag" ] - then - echo "✅ Documentation for "$tag" already exists."; - else - if [ "$tag" == "latest" ]; then - git checkout -f "$(git tag | tail -n 1)"; - else - git checkout -f "$tag"; - fi - - swift package \ - --allow-writing-to-directory docs-out/"$tag" \ - generate-documentation \ - --target ProjectDescription \ - --output-path docs-out/"$tag" \ - --transform-for-static-hosting \ - --hosting-base-path /tuist/"$tag" \ - && echo "✅ Documentation generated for "$tag" release." \ - || echo "⚠️ Documentation skipped for "$tag"."; - fi; - done - - - name: Fix permissions - run: 'sudo chown -R $USER docs-out' - - name: Publish documentation to GitHub Pages - uses: JamesIves/github-pages-deploy-action@4.1.7 - with: - branch: gh-pages - folder: docs-out - single-commit: true diff --git a/.github/workflows/deploy-tuist-docc-dry-run.yml b/.github/workflows/deploy-tuist-docc-dry-run.yml index 9c1f14357f6..987500647b8 100644 --- a/.github/workflows/deploy-tuist-docc-dry-run.yml +++ b/.github/workflows/deploy-tuist-docc-dry-run.yml @@ -1,14 +1,10 @@ -name: Deploy Tuist Docc dry-run +name: Deploy Tuist DocC dry-run on: - push: - branches: - - main pull_request: paths: - - Package.swift - - Package.resolved - - Sources/**/*.docc + - Sources/ProjectDescription/** + - docs/** - .github/workflows/deploy-tuist-docc-dry-run.yml jobs: @@ -18,7 +14,15 @@ jobs: steps: - name: Checkout Package uses: actions/checkout@v3 + - name: Checkout all tuist versions + uses: actions/checkout@v3 + with: + fetch-depth: 0 + path: tuist-archive - name: Select Xcode run: sudo xcode-select -switch /Applications/Xcode_$(cat .xcode-version).app + - uses: jdx/mise-action@v2 + with: + experimental: true - name: Build documentation - run: make docs/build + run: bash ./make/tasks/github/build-docc.sh diff --git a/.github/workflows/deploy-tuist-docc.yml b/.github/workflows/deploy-tuist-docc.yml index 1f4efce321b..ef75ee2c2bc 100644 --- a/.github/workflows/deploy-tuist-docc.yml +++ b/.github/workflows/deploy-tuist-docc.yml @@ -1,11 +1,9 @@ -# Build and deploy DocC to GitHub pages. Based off of Pointfree's work here: +# Build and deploy DocC to GitHub pages. Based off of PointFree's work here: # https://github.com/pointfreeco/swift-parsing/blob/main/.github/workflows/documentation.yml -name: Deploy Tuist Docc +name: Deploy Tuist DocC on: -# release: -# types: -# - published + workflow_dispatch: {} push: branches: - main @@ -27,22 +25,29 @@ jobs: step: start - name: Checkout Package uses: actions/checkout@v3 + - name: Checkout all tuist versions + uses: actions/checkout@v3 with: fetch-depth: 0 + path: tuist-archive - name: Select Xcode run: sudo xcode-select -switch /Applications/Xcode_$(cat .xcode-version).app + - uses: jdx/mise-action@v2 + with: + experimental: true - name: Build documentation - run: make docs/build + run: bash ./make/tasks/github/build-docc.sh - name: Fix permissions run: 'sudo chown -R $USER .build/documentation' - - name: Deploy production to Netlify - uses: South-Paw/action-netlify-cli@1.0.1 - id: netlify + - name: Publish to Cloudflare Pages + uses: cloudflare/pages-action@v1 with: - args: 'deploy --json --prod --dir \"./.build/documentation\" --message \"production [${{ github.sha }}]\"' - env: - NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} - NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }} + apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} + accountId: cc0237353f2f825680b0463629cd4a86 + projectName: tuist-docs + directory: .build/documentation + gitHubToken: ${{ secrets.GITHUB_TOKEN }} + wranglerVersion: '3' - name: Finish deployment uses: bobheadxi/deployments@v1 if: always() @@ -51,4 +56,4 @@ jobs: step: finish status: ${{ job.status }} deployment_id: ${{ steps.deployment.outputs.deployment_id }} - env_url: ${{ steps.netlify.outputs.NETLIFY_PROD_URL }} + env_url: "https://docs.tuist.io" diff --git a/.github/workflows/tuist.yml b/.github/workflows/tuist.yml index 039007a96f2..8b2c374de35 100644 --- a/.github/workflows/tuist.yml +++ b/.github/workflows/tuist.yml @@ -6,6 +6,7 @@ on: - main pull_request: paths: + - .xcode-version - Tuist/** - Package.resolved - Gemfile* @@ -15,7 +16,6 @@ on: - '!Sources/**/*.docc' - Templates/** - Tests/** - - features/** - fixtures/** - .package.resolved - .github/workflows/tuist.yml @@ -25,34 +25,9 @@ concurrency: cancel-in-progress: true env: - RUBY_VERSION: '3.0.3' - TUIST_STATS_OPT_OUT: true - NODE_VERSION: 16.17.0 TUIST_CONFIG_CLOUD_TOKEN: ${{ secrets.TUIST_CONFIG_CLOUD_TOKEN }} jobs: - release_build: - name: Release build with Xcode - runs-on: macos-13 - steps: - - uses: actions/checkout@v3 - - uses: actions/cache@v3 - name: 'Cache Tuist .build folder' - with: - path: .build - key: ${{ runner.os }}-${{ hashFiles('.xcode-version') }}-spm-v1-${{ hashFiles('Package.resolved') }}-git-${{ github.sha }} - restore-keys: | - ${{ runner.os }}-${{ hashFiles('.xcode-version') }}-spm-v1-${{ hashFiles('Package.resolved') }} - ${{ runner.os }}-${{ hashFiles('.xcode-version') }}-spm-v1 - - name: Select Xcode for Tuist and Tuistenv - run: sudo xcode-select -switch /Applications/Xcode_$(cat .xcode-version).app - - name: Build Tuist for release - run: swift build -c release --product tuist - - name: Build Tuistenv for release - run: swift build -c release --product tuistenv - - name: Build ProjectDescription for release - run: swift build -c release --product ProjectDescription - test: name: Test with Xcode runs-on: macos-13 @@ -60,7 +35,9 @@ jobs: - uses: actions/checkout@v3 - name: Select Xcode run: sudo xcode-select -switch /Applications/Xcode_$(cat .xcode-version).app - - uses: asdf-vm/actions/install@v2 + - uses: jdx/mise-action@v2 + with: + experimental: true - name: Select Xcode run: sudo xcode-select -switch /Applications/Xcode_$(cat .xcode-version).app - uses: actions/cache@v3 @@ -72,35 +49,22 @@ jobs: - name: Fetch dependencies run: tuist fetch - name: Test - run: tuist test --no-cache --skip-test-targets TuistBuildAcceptanceTests TuistGenerateOneAcceptanceTests TuistTestAcceptanceTests + run: tuist test --xcframeworks --skip-test-targets TuistBuildAcceptanceTests TuistDependenciesAcceptanceTests TuistGenerateAcceptanceTests TuistTestAcceptanceTests TuistAcceptanceTests --result-bundle-path /tmp/tuist/test-${{ matrix.feature }} + - uses: actions/upload-artifact@v4 + if: ${{ always() }} + with: + name: test-${{ matrix.feature }} + path: | + /tmp/tuist/** cache-warm: name: Cache warm with latest Tuist runs-on: macos-13 steps: - uses: actions/checkout@v3 - - uses: asdf-vm/actions/install@v2 - - name: Select Xcode - run: sudo xcode-select -switch /Applications/Xcode_$(cat .xcode-version).app - - uses: actions/cache@v3 - name: 'Cache fetched dependencies folder' + - uses: jdx/mise-action@v2 with: - path: Tuist/Dependencies/SwiftPackageManager/.build - key: spm-v1-${{ hashFiles('Package.resolved') }} - restore-keys: spm-v1-${{ hashFiles('Package.resolved') }} - - name: Fetch dependencies - run: tuist fetch - - name: Print hashes - run: tuist cache print-hashes - - name: Cache warm - run: tuist cache warm - - cache-warm-silicon: - name: Cache warm with latest Tuist on Silicon - runs-on: macos-13-xlarge - steps: - - uses: actions/checkout@v3 - - uses: asdf-vm/actions/install@v2 + experimental: true - name: Select Xcode run: sudo xcode-select -switch /Applications/Xcode_$(cat .xcode-version).app - uses: actions/cache@v3 @@ -112,12 +76,18 @@ jobs: - name: Fetch dependencies run: tuist fetch - name: Print hashes - run: tuist cache print-hashes + run: tuist cache print-hashes --xcframeworks - name: Cache warm - run: tuist cache warm + run: tuist cache warm --xcframeworks + - uses: actions/upload-artifact@v4 + if: ${{ always() }} + with: + name: cache-warm-x86_64.xcodebuild.log + path: | + /tmp/tuist/** acceptance_tests: - name: ${{ matrix.feature }} acceptance tests with Tuist + name: Run ${{ matrix.feature }} runs-on: macos-13 env: TUIST_CONFIG_CLOUD_TOKEN: ${{ secrets.TUIST_CONFIG_CLOUD_TOKEN }} @@ -125,18 +95,24 @@ jobs: matrix: feature: [ - 'Build', - 'GenerateOne', - 'Test', + 'TuistAcceptanceTests', + 'TuistBuildAcceptanceTests', + 'TuistDependenciesAcceptanceTests', + 'TuistGenerateAcceptanceTests', + 'TuistTestAcceptanceTests', ] steps: - uses: actions/checkout@v4 - - uses: asdf-vm/actions/install@v2 + - uses: jdx/mise-action@v2 + with: + experimental: true - name: Select Xcode # Xcode accepts -skipMacroValidation run: sudo xcode-select -switch /Applications/Xcode_$(cat .xcode-version).app - name: Skip Xcode Macro Fingerprint Validation run: defaults write com.apple.dt.Xcode IDESkipMacroFingerprintValidation -bool YES + - name: Skip Xcode Package Validation + run: defaults write com.apple.dt.Xcode IDESkipPackagePluginFingerprintValidatation -bool YES - uses: actions/cache@v3 name: 'Cache fetched dependencies folder' with: @@ -146,75 +122,25 @@ jobs: - name: Fetch dependencies run: tuist fetch - name: Run acceptance tests - run: tuist test Tuist${{ matrix.feature }}AcceptanceTests - - cucumber_acceptance_tests: - name: ${{ matrix.feature }} acceptance tests with Xcode - runs-on: macos-13 - strategy: - matrix: - feature: - [ - 'dependencies', - 'edit', - 'generate-3', - 'generate-4', - 'generate-5', - 'generate-6', - 'generate-7', - 'generate-8', - 'graph', - 'init', - 'list-targets', - 'plugins', - 'precompiled', - 'run', - 'scaffold', - 'tasks', - 'plugin', - ] - needs: release_build - steps: - - uses: actions/checkout@v3 - - name: Initialize submodules - run: | - git submodule update --init fixtures/tuist_plugin - - name: Select Xcode - run: sudo xcode-select -switch /Applications/Xcode_$(cat .xcode-version).app - - uses: actions/cache@v3 - name: 'Cache Tuist .build folder' + run: tuist test --xcframeworks --result-bundle-path /tmp/tuist/test-${{ matrix.feature }} ${{ matrix.feature }} + - uses: actions/upload-artifact@v4 + if: ${{ always() }} with: - path: .build - key: ${{ runner.os }}-${{ hashFiles('.xcode-version') }}-spm-v1-${{ hashFiles('Package.resolved') }}-git-${{ github.sha }} - restore-keys: | - ${{ runner.os }}-${{ hashFiles('.xcode-version') }}-spm-v1-${{ hashFiles('Package.resolved') }} - ${{ runner.os }}-${{ hashFiles('.xcode-version') }}-spm-v1 - - uses: ruby/setup-ruby@v1 - with: - ruby-version: ${{ env.RUBY_VERSION }} - - uses: actions/cache@v3 - with: - path: vendor/bundle - key: ${{ runner.os }}-${{ env.RUBY_VERSION }}-gems-${{ hashFiles('Gemfile.lock') }} - restore-keys: | - ${{ runner.os }}-${{ env.RUBY_VERSION }}-gems- - - name: Bundle install - run: | - bundle config path vendor/bundle - bundle install --jobs 4 --retry 3 - - name: Run cucumber tests - run: FEATURE=features/${{ matrix.feature }}.feature make tuist/acceptance-test - + name: test-${{ matrix.feature }} + path: | + /tmp/tuist/** lint: name: Lint runs-on: macos-13 steps: - uses: actions/checkout@v3 - - uses: asdf-vm/actions/install@v2 + - uses: jdx/mise-action@v2 + with: + experimental: true - name: Select Xcode run: sudo xcode-select -switch /Applications/Xcode_$(cat .xcode-version).app - name: Run - run: make workspace/lint + run: mise run lint lint-lockfiles: name: Lint lockfiles diff --git a/.mise.toml b/.mise.toml new file mode 100644 index 00000000000..dabcf39a9d5 --- /dev/null +++ b/.mise.toml @@ -0,0 +1,43 @@ +[tools] +tuist = "3.42.2" +swiftlint = "0.54.0" +swiftformat = "0.53.0" + +[plugins] +python = 'https://github.com/asdf-community/asdf-tuist' + +[tasks."docs:preview"] +description = 'Preview the documentation' +run = "./make/tasks/docs/preview.sh" + +[tasks."docs:build"] +description = "Build the documentation" +run = "./make/tasks/docs/build.sh" + +[tasks."tuist:edit"] +description = "Edit the project using Tuist" +run = "./make/tasks/tuist/edit.sh" + +[tasks."tuist:generate"] +description = "Generate the project using Tuist" +run = "./make/tasks/tuist/generate.sh" + +[tasks."tuist:build"] +description = "Build the project using Tuist" +run = "./make/tasks/tuist/build.sh" + +[tasks."tuist:test"] +description = "Test the project using Tuist" +run = "./make/tasks/tuist/test.sh" + +[tasks."tuist:run"] +description = "Run the project using Tuist" +run = "./make/tasks/tuist/run.sh" + +[tasks."lint"] +description = "Lint the workspace" +run = "./make/tasks/workspace/lint.sh" + +[tasks."lint:fix"] +description = "Lint the workspace fixing the issues" +run = "./make/tasks/workspace/lint-fix.sh" \ No newline at end of file diff --git a/.rubocop.yml b/.rubocop.yml deleted file mode 100644 index 463b5a60fdb..00000000000 --- a/.rubocop.yml +++ /dev/null @@ -1,28 +0,0 @@ -inherit_gem: - rubocop-shopify: rubocop.yml - rubocop-rails_config: - - config/rails.yml -require: - - rubocop-minitest - - rubocop-rails - -Style/ClassAndModuleChildren: - Enabled: false -Style/RedundantBegin: - Enabled: false -Lint/MissingSuper: - Enabled: false - -AllCops: - NewCops: enable - TargetRubyVersion: 2.5 - DisplayCopNames: true - DisplayStyleGuide: true - Include: - - Gemfile - - Exclude: - - node_modules/**/* - - vendor/**/* - - db/schema.rb - - bin/**/* diff --git a/.ruby-gemset b/.ruby-gemset deleted file mode 100644 index 947c621f74a..00000000000 --- a/.ruby-gemset +++ /dev/null @@ -1 +0,0 @@ -tuist diff --git a/.ruby-version b/.ruby-version deleted file mode 100644 index 818bd47abfc..00000000000 --- a/.ruby-version +++ /dev/null @@ -1 +0,0 @@ -3.0.6 diff --git a/.swiftformat b/.swiftformat index bac5e975137..6446fec651d 100644 --- a/.swiftformat +++ b/.swiftformat @@ -1,11 +1,12 @@ # file options --symlinks ignore ---exclude Tests/XCTestManifests.swift,features,Sources/TuistSupport/Vendored,fixtures/Targets/SlothCreator,Sources/TuistAutomation/XcodeBuild/XcodeBuildController.swift +--exclude Tests/XCTestManifests.swift,Sources/TuistSupport/Vendored,fixtures/Targets/SlothCreator,Sources/TuistAutomation/XcodeBuild/XcodeBuildController.swift --exclude fixtures/tuist_plugin --disable hoistAwait --disable hoistTry --swiftversion 5.7 +--minversion 0.53.0 # format options diff --git a/.swiftformat-version b/.swiftformat-version index 0afe921c6ea..7f422a161ae 100644 --- a/.swiftformat-version +++ b/.swiftformat-version @@ -1 +1 @@ -0.48.18 +0.53.0 diff --git a/.tool-versions b/.tool-versions deleted file mode 100644 index 18e7d61144e..00000000000 --- a/.tool-versions +++ /dev/null @@ -1,3 +0,0 @@ -tuist 3.35.5 -swiftlint 0.54.0 -swiftformat 0.52.10 diff --git a/.tuistignore b/.tuistignore index db2aaa5ea8d..1ba653aa84e 100644 --- a/.tuistignore +++ b/.tuistignore @@ -1,3 +1,5 @@ -fixtures .build -Tests \ No newline at end of file +docs +fixtures +Sources/ProjectDescription +Tests diff --git a/.xcode-version b/.xcode-version index 9dc738e691e..ccc2f3b87fe 100644 --- a/.xcode-version +++ b/.xcode-version @@ -1 +1 @@ -15.0.1 \ No newline at end of file +15.1 \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 564d8e5002d..a7f6cc4531d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,250 @@ # Changelog +## 3.42.2 - 2024-01-30 + +### Tuist + +#### Fixed + +- Fix parsing of `conditon` on SPM `.product` dependencies [#5846](https://github.com/tuist/tuist/pull/5846) by [@waltflanagan](https://github.com/waltflanagan) +- Fix missing plugin executable when the macro framework/library is not precompiled but the executable is [#5849](https://github.com/tuist/tuist/pull/5849) by [@pepicrft](https://github.com/pepicrft) +- Fix Swift Macros unresolved by the Xcode editor [#5851](https://github.com/tuist/tuist/pull/5851) by [@pepicrft](https://github.com/pepicrft) +- Fix false static side effect positives when the project contains Swift Macros [#5850](https://github.com/tuist/tuist/pull/5850) by [@pepicrft](https://github.com/pepicrft) + +### Tuist Cloud + +- no changes + +## 3.42.1 - 2024-01-26 + +### Tuist + +#### Changed + +- Improve editing of PackageSettings in Package.swift [#5837](https://github.com/tuist/tuist/pull/5837) by [@fortmarek](https://github.com/fortmarek) + +#### Fixed + +- Not bundle Swift Macros into the parent framework [#5834](https://github.com/tuist/tuist/pull/5834) by [@pepicrft](https://github.com/pepicrft) + +### Tuist Cloud + +#### Changed + +- Remove the the warning to nudge users to cache using `--xcframeworks` by [@pepicrft](https://github.com/pepicrft) + +## 3.42.0 - 2024-01-24 + +### Tuist + +#### Fixed + +- Rename Compiled group to Frameworks [#5826](https://github.com/tuist/tuist/pull/5826) by [@kwridan](https://github.com/kwridan) +- Use the correct Swift tools version when loading the PackageSettings [#5831](https://github.com/tuist/tuist/pull/5831) by [@fortmarek](https://github.com/fortmarek) + +### Tuist Cloud + +- no changes + +## 3.41.0 - 2024-01-19 + +### Tuist + +#### Added + +- Add PackageSettings for Package.swift [#5802](https://github.com/tuist/tuist/pull/5802) by [@fortmarek](https://github.com/fortmarek) + +#### Fixed + +- Fix generation not including macros from XCFrameworks [#5801](https://github.com/tuist/tuist/pull/5801) by [@pepicrft](https://github.com/pepicrft) +- Fix loading PackageSettings when not specified in Package.swift [#5805](https://github.com/tuist/tuist/pull/5805) by [@fortmarek](https://github.com/fortmarek) + +### Tuist Cloud + +- no changes + +## 3.40.0 - 2024-01-15 + +### Tuist + +#### Changed + +- Remove raw xcodebuild logs in favor of resultBundlePath and xcactivitylog [#5776](https://github.com/tuist/tuist/pull/5776) by [@fortmarek](https://github.com/fortmarek) + +#### Fixed + +- Fix incremental test execution when migrating from Tuist versions older than 3.36.0 [#5791](https://github.com/tuist/tuist/pull/5791) by [@pepicrft](https://github.com/pepicrft) + +### Tuist Cloud + +#### Changed + +- Build macOS xcframework for both Intel and ARM by [@fortmarek](https://github.com/fortmarek) + +## 3.39.3 - 2024-01-08 + +### Tuist + +- no changes + +### Tuist Cloud + +- no changes + +## 3.39.2 - 2024-01-08 + +### Tuist + +- no changes + +### Tuist Cloud + +#### Fixed + +- Fix the warning messages that tells users about the depreaction of frameworks for caching by [@pepicrft](https://github.com/pepicrft) +- Fix cache warming failing to resolve the hash for targets by [@pepicrft](https://github.com/pepicrft) + +## 3.39.1 - 2024-01-08 + +### Tuist + +- no changes + +### Tuist Cloud + +#### Fixed + +- Fix cache warm for xcframeworks when explicit dependencies are enabled. [#66](https://github.com/tuist/tuist/pull/66) by [@fortmarek](https://github.com/fortmarek) + +## 3.39.0 - 2024-01-07 + +### Tuist + +#### Added + +- Prefer an exact match on `developmentRegion` when choosing localization strings files [#5758](https://github.com/tuist/tuist/pull/5758) by [@UniekLee](https://github.com/UniekLee) +- Verbose-log the `xcodebuild` command that's being executed [#5768](https://github.com/tuist/tuist/pull/5768) by [@pepicrft](https://github.com/pepicrft) +- Add SkippedTests for TestAction [#5767](https://github.com/tuist/tuist/pull/5767) by [@dp221125](https://github.com/dp221125) + +#### Fixed + +- Fix explicit dependencies for archive [#5764](https://github.com/tuist/tuist/pull/5764) by [@fortmarek](https://github.com/fortmarek) +- Add support for transitive Swift Macros [#5772](https://github.com/tuist/tuist/pull/5772) by [@pepicrft](https://github.com/pepicrft) +- Optimize the graph traverser function to obtain the platforms for external dependencies [#5770](https://github.com/tuist/tuist/pull/5770) by [@pepicrft](https://github.com/pepicrft) + +### Tuist Cloud + +- no changes + +## 3.38.0 - 2024-01-03 + +### Tuist + +#### Changed + +- Make `ProjectDescription` model attributes mutable [#5745](https://github.com/tuist/tuist/pull/5745) by [@dxmvsh](https://github.com/dxmvsh) + +#### Added + +- Add a `--generate-only` flag to the build command [#5711](https://github.com/tuist/tuist/pull/5711) by [@pepicrft](https://github.com/pepicrft) +- Add enforceExplicitDependendencies generation option [#5698](https://github.com/tuist/tuist/pull/5698) by [@fortmarek](https://github.com/fortmarek) +- Add Package.swift support for tuist edit [#5751](https://github.com/tuist/tuist/pull/5751) by [@fortmarek](https://github.com/fortmarek) + +#### Fixed + +- Align the behaviour of `--path` in `tuist fetch` with the rest of the CLI [#5742](https://github.com/tuist/tuist/pull/5742) by [@danibachar](https://github.com/danibachar) + +### Tuist Cloud + +- no changes + +## 3.37.0 - 2023-12-28 + +### Tuist + +- no changes + +### Tuist Cloud + +- no changes + +## 3.36.5 - 2023-12-24 + +### Tuist + +- no changes + +### Tuist Cloud + +- no changes + +## 3.36.4 - 2023-12-23 + +### Tuist + +- no changes + +### Tuist Cloud + +- no changes + +## 3.36.3 - 2023-12-22 + +### Tuist + +#### Fixed + +- Fix deployment targets on tree shake [#5705](https://github.com/tuist/tuist/pull/5705) by [@alexanderwe](https://github.com/alexanderwe) + +### Tuist Cloud + +- no changes + +## 3.36.2 - 2023-12-21 + +### Tuist + +#### Added + +- [Multiplatform] Enhance build phases to account for platform conditions [#5691](https://github.com/tuist/tuist/pull/5691) by [@alexanderwe](https://github.com/alexanderwe) + +#### Fixed + +- Fix a bug that causes platform from being wrongly stripped [#5704](https://github.com/tuist/tuist/pull/5704) by [@pepicrft](https://github.com/pepicrft) + +### Tuist Cloud + +- no changes + +## 3.36.1 - 2023-12-20 + +### Tuist + +#### Added + +- Export the `TuistDependencies` product [#5697](https://github.com/tuist/tuist/pull/5697) by [@pepicrft](https://github.com/pepicrft) + +#### Fixed + +- Include `XCTVapor` in a predefined list of testing packages [#5687](https://github.com/tuist/tuist/pull/5687) by [@danielmoro](https://github.com/danielmoro) + +### Tuist Cloud + +- no changes + +## 3.36.0 - 2023-12-15 + +### Tuist + +#### Fixed + +- Prevent duplicated warnings [#5662](https://github.com/tuist/tuist/pull/5662) by [@pepicrft](https://github.com/pepicrft) +- Fix --skip-test-targets with a test class [#5673](https://github.com/tuist/tuist/pull/5673) by [@fortmarek](https://github.com/fortmarek) + +### Tuist Cloud + +- no changes + ## 3.35.5 - 2023-12-10 ### Fixed diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e545a53550a..e4e9e144366 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,8 +2,8 @@ By submitting a pull request, you represent that you have the right to license y --- -For more information about how to contribute, please refer to the [Contributors -> Get started](https://docs.next.tuist.io/documentation/tuist/contributing---get-started) section of the documentation. +For more information about how to contribute, please refer to the [Contributors -> Get started](https://docs.tuist.io/documentation/tuist/get-started-as-contributor) section of the documentation. --- -Before submitting the pull request, please make sure you have [tested your changes](https://docs.tuist.io/contributors/testing-strategy/) and that they follow the [Tuist project Manifesto](https://docs.next.tuist.io/documentation/tuist/contributing---manifesto). +Before submitting the pull request, please make sure you have [tested your changes](https://docs.old.tuist.io/contributors/testing-strategy/) and that they follow the [Tuist project Manifesto](https://docs.tuist.io/documentation/tuist/manifesto). diff --git a/Gemfile b/Gemfile deleted file mode 100644 index 1a09ff17cd0..00000000000 --- a/Gemfile +++ /dev/null @@ -1,40 +0,0 @@ -# frozen_string_literal: true - -source "https://rubygems.org" - -gem "cucumber", "~> 6.0" -gem "rake", "~> 13.0" -gem "simctl", "~> 1.6" -gem "google-cloud-storage", "~> 1.31" -gem "colorize", "~> 0.8.1" -gem "xcodeproj", "~> 1.19" -gem "highline", "~> 2.0" -gem "rubyzip", "~> 2.3.0" -gem "ruby-macho", "~> 1.4" -gem "cli-ui", "~> 1.5" -gem "thor", "~> 1.1" -gem "octokit", "~> 4.21" -gem "zeitwerk", "~> 2.4" -gem "cli-kit", "~> 3.3" -gem "semantic", "~> 1.6" -gem "down", "~> 5.2" -gem "ejson", "~> 1.2" - -group :test do - gem "mocha", "~> 1.12" - gem "minitest" - gem "minitest-reporters" -end - -group :development do - gem "rubocop-shopify" - gem "rubocop-minitest", "~> 0.12.1" - gem "rubocop-rake", "~> 0.5.1" - gem "rubocop", "~> 1.14" - gem "rubocop-rails", "~> 2.13.0" - gem "rubocop-rails_config" -end - -group :development, :test do - gem "byebug", "~> 11.1" -end diff --git a/Gemfile.lock b/Gemfile.lock deleted file mode 100644 index c0dc864dc79..00000000000 --- a/Gemfile.lock +++ /dev/null @@ -1,293 +0,0 @@ -GEM - remote: https://rubygems.org/ - specs: - CFPropertyList (3.0.6) - rexml - actionpack (7.0.5.1) - actionview (= 7.0.5.1) - activesupport (= 7.0.5.1) - rack (~> 2.0, >= 2.2.4) - rack-test (>= 0.6.3) - rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.0, >= 1.2.0) - actionview (7.0.5.1) - activesupport (= 7.0.5.1) - builder (~> 3.1) - erubi (~> 1.4) - rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.1, >= 1.2.0) - activesupport (7.0.5.1) - concurrent-ruby (~> 1.0, >= 1.0.2) - i18n (>= 1.6, < 2) - minitest (>= 5.1) - tzinfo (~> 2.0) - addressable (2.8.4) - public_suffix (>= 2.0.2, < 6.0) - ansi (1.5.0) - ast (2.4.2) - atomos (0.1.3) - builder (3.2.4) - byebug (11.1.3) - claide (1.1.0) - cli-kit (3.3.0) - cli-ui (>= 1.1.4) - cli-ui (1.5.1) - colored2 (3.1.2) - colorize (0.8.1) - concurrent-ruby (1.2.2) - crass (1.0.6) - cucumber (6.1.0) - builder (~> 3.2, >= 3.2.4) - cucumber-core (~> 9.0, >= 9.0.1) - cucumber-create-meta (~> 4.0, >= 4.0.0) - cucumber-cucumber-expressions (~> 12.1, >= 12.1.1) - cucumber-gherkin (~> 18.1, >= 18.1.0) - cucumber-html-formatter (~> 13.0, >= 13.0.0) - cucumber-messages (~> 15.0, >= 15.0.0) - cucumber-wire (~> 5.0, >= 5.0.1) - diff-lcs (~> 1.4, >= 1.4.4) - mime-types (~> 3.3, >= 3.3.1) - multi_test (~> 0.1, >= 0.1.2) - sys-uname (~> 1.2, >= 1.2.2) - cucumber-core (9.0.1) - cucumber-gherkin (~> 18.1, >= 18.1.0) - cucumber-messages (~> 15.0, >= 15.0.0) - cucumber-tag-expressions (~> 3.0, >= 3.0.1) - cucumber-create-meta (4.0.0) - cucumber-messages (~> 15.0, >= 15.0.0) - sys-uname (~> 1.2, >= 1.2.2) - cucumber-cucumber-expressions (12.1.3) - cucumber-gherkin (18.1.1) - cucumber-messages (~> 15.0, >= 15.0.0) - cucumber-html-formatter (13.0.0) - cucumber-messages (~> 15.0, >= 15.0.0) - cucumber-messages (15.0.0) - protobuf-cucumber (~> 3.10, >= 3.10.8) - cucumber-tag-expressions (3.0.1) - cucumber-wire (5.0.1) - cucumber-core (~> 9.0, >= 9.0.1) - cucumber-cucumber-expressions (~> 12.1, >= 12.1.1) - cucumber-messages (~> 15.0, >= 15.0.0) - declarative (0.0.20) - diff-lcs (1.5.0) - digest-crc (0.6.4) - rake (>= 12.0.0, < 14.0.0) - down (5.4.1) - addressable (~> 2.8) - ejson (1.3.1) - erubi (1.12.0) - faraday (2.7.7) - faraday-net_http (>= 2.0, < 3.1) - ruby2_keywords (>= 0.0.4) - faraday-net_http (3.0.2) - ffi (1.15.5) - google-apis-core (0.11.0) - addressable (~> 2.5, >= 2.5.1) - googleauth (>= 0.16.2, < 2.a) - httpclient (>= 2.8.1, < 3.a) - mini_mime (~> 1.0) - representable (~> 3.0) - retriable (>= 2.0, < 4.a) - rexml - webrick - google-apis-iamcredentials_v1 (0.17.0) - google-apis-core (>= 0.11.0, < 2.a) - google-apis-storage_v1 (0.19.0) - google-apis-core (>= 0.9.0, < 2.a) - google-cloud-core (1.6.0) - google-cloud-env (~> 1.0) - google-cloud-errors (~> 1.0) - google-cloud-env (1.6.0) - faraday (>= 0.17.3, < 3.0) - google-cloud-errors (1.3.1) - google-cloud-storage (1.44.0) - addressable (~> 2.8) - digest-crc (~> 0.4) - google-apis-iamcredentials_v1 (~> 0.1) - google-apis-storage_v1 (~> 0.19.0) - google-cloud-core (~> 1.6) - googleauth (>= 0.16.2, < 2.a) - mini_mime (~> 1.0) - googleauth (1.6.0) - faraday (>= 0.17.3, < 3.a) - jwt (>= 1.4, < 3.0) - memoist (~> 0.16) - multi_json (~> 1.11) - os (>= 0.9, < 2.0) - signet (>= 0.16, < 2.a) - highline (2.1.0) - httpclient (2.8.3) - i18n (1.14.1) - concurrent-ruby (~> 1.0) - json (2.6.3) - jwt (2.7.1) - language_server-protocol (3.17.0.3) - loofah (2.21.3) - crass (~> 1.0.2) - nokogiri (>= 1.12.0) - memoist (0.16.2) - method_source (1.0.0) - middleware (0.1.0) - mime-types (3.4.1) - mime-types-data (~> 3.2015) - mime-types-data (3.2023.0218.1) - mini_mime (1.1.2) - mini_portile2 (2.8.2) - minitest (5.18.1) - minitest-reporters (1.6.0) - ansi - builder - minitest (>= 5.0) - ruby-progressbar - mocha (1.16.1) - multi_json (1.15.0) - multi_test (0.1.2) - nanaimo (0.3.0) - naturally (2.2.1) - nokogiri (1.15.2) - mini_portile2 (~> 2.8.2) - racc (~> 1.4) - octokit (4.25.1) - faraday (>= 1, < 3) - sawyer (~> 0.9) - os (1.1.4) - parallel (1.23.0) - parser (3.2.2.3) - ast (~> 2.4.1) - racc - protobuf-cucumber (3.10.8) - activesupport (>= 3.2) - middleware - thor - thread_safe - public_suffix (5.0.1) - racc (1.7.1) - rack (2.2.7) - rack-test (2.1.0) - rack (>= 1.3) - rails-dom-testing (2.0.3) - activesupport (>= 4.2.0) - nokogiri (>= 1.6) - rails-html-sanitizer (1.6.0) - loofah (~> 2.21) - nokogiri (~> 1.14) - railties (7.0.5.1) - actionpack (= 7.0.5.1) - activesupport (= 7.0.5.1) - method_source - rake (>= 12.2) - thor (~> 1.0) - zeitwerk (~> 2.5) - rainbow (3.1.1) - rake (13.0.6) - regexp_parser (2.8.1) - representable (3.2.0) - declarative (< 0.1.0) - trailblazer-option (>= 0.1.1, < 0.2.0) - uber (< 0.2.0) - retriable (3.1.2) - rexml (3.2.5) - rubocop (1.53.0) - json (~> 2.3) - language_server-protocol (>= 3.17.0) - parallel (~> 1.10) - parser (>= 3.2.2.3) - rainbow (>= 2.2.2, < 4.0) - regexp_parser (>= 1.8, < 3.0) - rexml (>= 3.2.5, < 4.0) - rubocop-ast (>= 1.28.0, < 2.0) - ruby-progressbar (~> 1.7) - unicode-display_width (>= 2.4.0, < 3.0) - rubocop-ast (1.29.0) - parser (>= 3.2.1.0) - rubocop-minitest (0.12.1) - rubocop (>= 0.90, < 2.0) - rubocop-packaging (0.5.2) - rubocop (>= 1.33, < 2.0) - rubocop-performance (1.18.0) - rubocop (>= 1.7.0, < 2.0) - rubocop-ast (>= 0.4.0) - rubocop-rails (2.13.2) - activesupport (>= 4.2.0) - rack (>= 1.1) - rubocop (>= 1.7.0, < 2.0) - rubocop-rails_config (1.6.1) - railties (>= 5.0) - rubocop (>= 1.18) - rubocop-ast (>= 1.0.1) - rubocop-packaging (~> 0.5) - rubocop-performance (~> 1.11) - rubocop-rails (~> 2.0) - rubocop-rake (0.5.1) - rubocop - rubocop-shopify (2.14.0) - rubocop (~> 1.51) - ruby-macho (1.4.0) - ruby-progressbar (1.13.0) - ruby2_keywords (0.0.5) - rubyzip (2.3.2) - sawyer (0.9.2) - addressable (>= 2.3.5) - faraday (>= 0.17.3, < 3) - semantic (1.6.1) - signet (0.17.0) - addressable (~> 2.8) - faraday (>= 0.17.5, < 3.a) - jwt (>= 1.5, < 3.0) - multi_json (~> 1.10) - simctl (1.6.10) - CFPropertyList - naturally - sys-uname (1.2.3) - ffi (~> 1.1) - thor (1.2.2) - thread_safe (0.3.6) - trailblazer-option (0.1.2) - tzinfo (2.0.6) - concurrent-ruby (~> 1.0) - uber (0.1.0) - unicode-display_width (2.4.2) - webrick (1.8.1) - xcodeproj (1.22.0) - CFPropertyList (>= 2.3.3, < 4.0) - atomos (~> 0.1.3) - claide (>= 1.0.2, < 2.0) - colored2 (~> 3.1) - nanaimo (~> 0.3.0) - rexml (~> 3.2.4) - zeitwerk (2.6.8) - -PLATFORMS - ruby - -DEPENDENCIES - byebug (~> 11.1) - cli-kit (~> 3.3) - cli-ui (~> 1.5) - colorize (~> 0.8.1) - cucumber (~> 6.0) - down (~> 5.2) - ejson (~> 1.2) - google-cloud-storage (~> 1.31) - highline (~> 2.0) - minitest - minitest-reporters - mocha (~> 1.12) - octokit (~> 4.21) - rake (~> 13.0) - rubocop (~> 1.14) - rubocop-minitest (~> 0.12.1) - rubocop-rails (~> 2.13.0) - rubocop-rails_config - rubocop-rake (~> 0.5.1) - rubocop-shopify - ruby-macho (~> 1.4) - rubyzip (~> 2.3.0) - semantic (~> 1.6) - simctl (~> 1.6) - thor (~> 1.1) - xcodeproj (~> 1.19) - zeitwerk (~> 2.4) - -BUNDLED WITH - 2.2.33 diff --git a/Makefile b/Makefile deleted file mode 100644 index 16f1346c45b..00000000000 --- a/Makefile +++ /dev/null @@ -1,25 +0,0 @@ -# Documentation -docs/preview: - ./make/tasks/docs/preview.sh -docs/build: - ./make/tasks/docs/build.sh - -# Tuist -tuist/edit: - ./make/tasks/tuist/edit.sh -tuist/generate: - ./make/tasks/tuist/generate.sh $(ARGS) -tuist/build: - ./make/tasks/tuist/build.sh $(ARGS) -tuist/test: - ./make/tasks/tuist/test.sh $(ARGS) -tuist/run: - ./make/tasks/tuist/run.sh $(ARGS) -tuist/acceptance-test: - ./make/tasks/tuist/acceptance-test.sh - -# Shared -workspace/lint: - ./make/tasks/workspace/lint.sh $(ARGS) -workspace/lint-fix: - ./make/tasks/workspace/lint-fix.sh $(ARGS) diff --git a/Package.resolved b/Package.resolved index 04ede1cf6ac..e8653e8167e 100644 --- a/Package.resolved +++ b/Package.resolved @@ -149,28 +149,10 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-argument-parser.git", "state" : { - "revision" : "8f4d2753f0e4778c76d5f05ad16c74f707390531", - "version" : "1.2.3" - } - }, - { - "identity" : "swift-docc-plugin", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-docc-plugin.git", - "state" : { - "revision" : "26ac5758409154cc448d7ab82389c520fa8a8247", + "revision" : "c8ed701b513cf5177118a175d85fbbbcd707ab41", "version" : "1.3.0" } }, - { - "identity" : "swift-docc-symbolkit", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-docc-symbolkit", - "state" : { - "revision" : "b45d1f2ed151d057b54504d653e0da5552844e34", - "version" : "1.0.0" - } - }, { "identity" : "swift-log", "kind" : "remoteSourceControl", @@ -218,10 +200,10 @@ { "identity" : "xcbeautify", "kind" : "remoteSourceControl", - "location" : "https://github.com/tuist/xcbeautify", + "location" : "https://github.com/cpisciotta/xcbeautify", "state" : { - "revision" : "7dc31d4a9e9cc660f2de6ff6c6e0f0d5dfbb572b", - "version" : "1.0.1" + "revision" : "84d24a9854e6fdcd2c91122d50a3189b072e8136", + "version" : "1.4.0" } }, { diff --git a/Package.swift b/Package.swift index 391cb537334..1ea02c28acd 100644 --- a/Package.swift +++ b/Package.swift @@ -140,7 +140,9 @@ var targets: [Target] = [ dependencies: [ "TuistKit", "TuistCore", + "TuistSupport", "TuistSupportTesting", + "XcodeProj", swiftToolsSupportDependency, ], linkerSettings: [.linkedFramework("XCTest")] @@ -342,6 +344,10 @@ let package = Package( name: "TuistSigning", targets: ["TuistSigning"] ), + .library( + name: "TuistDependencies", + targets: ["TuistDependencies"] + ), .library( name: "TuistAcceptanceTesting", targets: ["TuistAcceptanceTesting"] @@ -364,7 +370,6 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/apple/swift-argument-parser", from: "1.2.3"), - .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.3.0"), .package(url: "https://github.com/apple/swift-log", from: "1.5.3"), .package(url: "https://github.com/apple/swift-tools-support-core", from: "0.6.1"), .package(url: "https://github.com/CombineCommunity/CombineExt", from: "1.8.1"), @@ -379,7 +384,7 @@ let package = Package( .package(url: "https://github.com/SwiftGen/StencilSwiftKit", exact: "2.10.1"), .package(url: "https://github.com/SwiftGen/SwiftGen", exact: "6.6.2"), .package(url: "https://github.com/tuist/XcodeProj", exact: "8.15.0"), - .package(url: "https://github.com/tuist/xcbeautify", from: "1.0.1"), + .package(url: "https://github.com/cpisciotta/xcbeautify", from: "1.4.0"), ], targets: targets ) diff --git a/Project.swift b/Project.swift index bfdc8ce3c26..d3aa4be0757 100644 --- a/Project.swift +++ b/Project.swift @@ -494,17 +494,19 @@ func targets() -> [Target] { product: .staticFramework, dependencies: [ .target(name: "TuistKit"), + .target(name: "TuistSupport"), .target(name: "TuistSupportTesting"), .target(name: "TuistCore"), .external(name: "SwiftToolsSupport"), .external(name: "SystemPackage"), + .external(name: "XcodeProj"), .sdk(name: "XCTest", type: .framework, status: .optional), ] ), ] } -let acceptanceTests: [(target: Target, scheme: Scheme)] = ["Build", "GenerateOne", "Test"].map { +let acceptanceTests: [(target: Target, scheme: Scheme)] = ["", "Build", "Dependencies", "Generate", "Test"].map { ( target: .target( name: "Tuist\($0)AcceptanceTests", @@ -556,8 +558,8 @@ let project = Project( additionalFiles: [ "CHANGELOG.md", "README.md", - "Sources/tuist/tuist.docc/**/*.md", - "Sources/tuist/tuist.docc/**/*.tutorial", - "Sources/tuist/tuist.docc/**/*.swift", + "docs/Sources/tuist/tuist.docc/**/*.md", + "docs/Sources/tuist/tuist.docc/**/*.tutorial", + "docs/Sources/tuist/tuist.docc/**/*.swift", ] ) diff --git a/README.md b/README.md index b7bc17cb608..80b562fa098 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ Backers License Powered by Tuist +
@@ -24,12 +25,16 @@ It's open source and written in Swift. ## Install ⬇️ -### Running script (Recommended) +### Recommended: [mise](https://github.com/jdx/mise) -```shell -curl -Ls https://install.tuist.io | bash +Install [mise](https://mise.jdx.dev/getting-started.html#quickstart) and then run the following command to install Tuist: + +```bash +mise install tuist ``` +You can check out [the documentation](https://docs.tuist.io/documentation/tuist/installation) to learn more about the rationale behind our installation approach and alternative approaches. + ## Bootstrap your first project 🌀 ```bash @@ -38,7 +43,7 @@ tuist generate # Generates Xcode project & workspace tuist build # Builds your project ``` -[Check out](https://docs.tuist.io) the project "Get Started" guide to learn more about Tuist and all its features. +[Check out](https://docs.tuist.io/documentation/tuist) the project "Get Started" guide to learn more about Tuist and all its features. ## Sample projects 🔬 @@ -46,7 +51,7 @@ You can find some sample projects in the [fixtures folder](fixtures) or the [awe ## Want to contribute? -You can use our [contribution docs](https://docs.tuist.io/contributors/get-started) to get started. If you don't have a specific issue in mind, we are more than happy to help you, just ask for help in a given issue or on our [Slack](https://join.slack.com/t/tuistapp/shared_invite/zt-1lqw355mp-zElRwLeoZ2EQsgGEkyaFgg). You can find good issues for first-time contributors [here](https://github.com/tuist/tuist/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22). We also offer [issue bounties](https://github.com/tuist/tuist/discussions/4982) for some highly-valued issues. +You can use our [contribution docs](https://docs.tuist.io/documentation/tuist/get-started-as-contributor) to get started. If you don't have a specific issue in mind, we are more than happy to help you, just ask for help in a given issue or on our [Slack](https://join.slack.com/t/tuistapp/shared_invite/zt-1lqw355mp-zElRwLeoZ2EQsgGEkyaFgg). You can find good issues for first-time contributors [here](https://github.com/tuist/tuist/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22). We also offer [issue bounties](https://github.com/tuist/tuist/discussions/4982) for some highly-valued issues. ## Sponsors @@ -214,11 +219,14 @@ The financial sustainability of the project is possible thanks to the ongoing co + + +
## Documentation 📝 -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://docs.tuist.io/tutorial/get-started/). +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://docs.tuist.io/documentation/tuist/). ## Supported by great companies @@ -232,7 +240,7 @@ Do you want to know more about what Tuist can offer you? Or perhaps want to cont ## Contribute 👩‍💻 -If you are interested in contributing to the project, our documentation has a section with resources for contributors. We recommend starting from [this page](https://docs.tuist.io/contributors/get-started). +If you are interested in contributing to the project, our documentation has a section with resources for contributors. We recommend starting from [this page](https://docs.tuist.io/documentation/tuist/get-started-as-contributor). ## Core Team ✨ @@ -408,44 +416,48 @@ Thanks goes to these wonderful people:
MontakOleg
-
안지훈

oozoofrog

Martin Strambach

sh-a-n

Batuhan Saka

SooHwanCho
+
Gary Riches
-
Gary Riches

mustiikhalil

Serhii Butenko

Petrukha Ivan

Mathias Schreck

Yen-Chia Lin

Mary
+
Hyunjin
-
Hyunjin

Kevin Aguilar

Andrew Roan

ibrahim oktay

Dmitriy Kulakov

Jaewon-Yun

tatagrigory
+
Denil Chungath
-
Denil Chungath

Victor Sarda

tzxdtc10

Thieme

Clemens Beck

Paul Taykalo

Vitaly Kravtsov
+
dc
-
dc

baegteun

Vinícius Couto Tasso
+
안지훈
+
Dimash
+
danibachar
+
한석호(MilKyo)
+
Hai Feng Kao
diff --git a/Sources/ProjectDescription/AnalyzeAction.swift b/Sources/ProjectDescription/AnalyzeAction.swift index 78e9c61ef33..d4f63e3520c 100644 --- a/Sources/ProjectDescription/AnalyzeAction.swift +++ b/Sources/ProjectDescription/AnalyzeAction.swift @@ -5,7 +5,7 @@ import Foundation /// It's initialized with the `.analyzeAction` static method public struct AnalyzeAction: Equatable, Codable { /// Indicates the build configuration the product should be analyzed with. - public let configuration: ConfigurationName + public var configuration: ConfigurationName /// Returns an analyze action. /// - Parameter configuration: Indicates the build configuration the product should be analyzed with. diff --git a/Sources/ProjectDescription/ArchiveAction.swift b/Sources/ProjectDescription/ArchiveAction.swift index e841e3f5cd2..771ebaa57e1 100644 --- a/Sources/ProjectDescription/ArchiveAction.swift +++ b/Sources/ProjectDescription/ArchiveAction.swift @@ -5,15 +5,15 @@ import Foundation /// It's initialized with the `.archiveAction` static method. public struct ArchiveAction: Equatable, Codable { /// Indicates the build configuration to run the archive with. - public let configuration: ConfigurationName + public var configuration: ConfigurationName /// If set to true, Xcode will reveal the Organizer on completion. - public let revealArchiveInOrganizer: Bool + public var revealArchiveInOrganizer: Bool /// Set if you want to override Xcode's default archive name. - public let customArchiveName: String? + public var customArchiveName: String? /// A list of actions that are executed before starting the archive process. - public let preActions: [ExecutionAction] + public var preActions: [ExecutionAction] /// A list of actions that are executed after the archive process. - public let postActions: [ExecutionAction] + public var postActions: [ExecutionAction] init( configuration: ConfigurationName, diff --git a/Sources/ProjectDescription/Arguments.swift b/Sources/ProjectDescription/Arguments.swift index 52b8ae86767..7e5456fa048 100644 --- a/Sources/ProjectDescription/Arguments.swift +++ b/Sources/ProjectDescription/Arguments.swift @@ -2,8 +2,8 @@ import Foundation /// A collection of arguments and environment variables. public struct Arguments: Equatable, Codable { - public let environmentVariables: [String: EnvironmentVariable] - public let launchArguments: [LaunchArgument] + public var environmentVariables: [String: EnvironmentVariable] + public var launchArguments: [LaunchArgument] @available(*, deprecated, message: "please use environmentVariables instead") public init( diff --git a/Sources/ProjectDescription/BuildAction.swift b/Sources/ProjectDescription/BuildAction.swift index 2fda7beaa0b..98149fac969 100644 --- a/Sources/ProjectDescription/BuildAction.swift +++ b/Sources/ProjectDescription/BuildAction.swift @@ -5,13 +5,13 @@ import Foundation /// It's initialized with the `.buildAction` static method. public struct BuildAction: Equatable, Codable { /// A list of targets to build, which are defined in the project. - public let targets: [TargetReference] + public var targets: [TargetReference] /// A list of actions that are executed before starting the build process. - public let preActions: [ExecutionAction] + public var preActions: [ExecutionAction] /// A list of actions that are executed after the build process. - public let postActions: [ExecutionAction] + public var postActions: [ExecutionAction] /// Whether the post actions should be run in the case of a failure - public let runPostActionsOnFailure: Bool + public var runPostActionsOnFailure: Bool public init( targets: [TargetReference], diff --git a/Sources/ProjectDescription/BuildRule.swift b/Sources/ProjectDescription/BuildRule.swift index 1a25710d1be..1e3c963c480 100644 --- a/Sources/ProjectDescription/BuildRule.swift +++ b/Sources/ProjectDescription/BuildRule.swift @@ -3,31 +3,31 @@ import Foundation /// A BuildRule is used to specify a method for transforming an input file in to an output file(s). public struct BuildRule: Codable, Equatable { /// Compiler specification for element transformation. - public let compilerSpec: CompilerSpec + public var compilerSpec: CompilerSpec /// Regex pattern when `sourceFilesWithNamesMatching` is used. - public let filePatterns: String? + public var filePatterns: String? /// File types which are processed by build rule. - public let fileType: FileType + public var fileType: FileType /// Build rule name. - public let name: String? + public var name: String? /// Build rule output files. - public let outputFiles: [String] + public var outputFiles: [String] /// Build rule input files. - public let inputFiles: [String] + public var inputFiles: [String] /// Build rule output files compiler flags. - public let outputFilesCompilerFlags: [String] + public var outputFilesCompilerFlags: [String] /// Build rule custom script when `customScript` is used. - public let script: String? + public var script: String? /// Build rule run once per architecture. - public let runOncePerArchitecture: Bool? + public var runOncePerArchitecture: Bool? public init( name: String? = nil, diff --git a/Sources/ProjectDescription/Cache.swift b/Sources/ProjectDescription/Cache.swift index c8ae40010bf..e6e6bead769 100644 --- a/Sources/ProjectDescription/Cache.swift +++ b/Sources/ProjectDescription/Cache.swift @@ -5,16 +5,16 @@ public struct Cache: Codable, Equatable { /// A cache profile. public struct Profile: Codable, Equatable { /// The unique name of a profile - public let name: String + public var name: String /// The configuration to be used when building the project during a caching warmup - public let configuration: String + public var configuration: String /// The device to be used when building the project during a caching warmup - public let device: String? + public var device: String? /// The version of the OS to be used when building the project during a caching warmup - public let os: String? + public var os: String? /// Returns a `Cache.Profile` instance. /// @@ -35,9 +35,9 @@ public struct Cache: Codable, Equatable { } /// A list of the cache profiles. - public let profiles: [Profile] + public var profiles: [Profile] /// The path where the cache will be stored, if `nil` it will be a default location in a shared directory. - public let path: Path? + public var path: Path? /// Returns a `Cache` instance containing the given profiles. /// If no profile list is provided, tuist's default profile will be taken as the default. diff --git a/Sources/ProjectDescription/Cloud.swift b/Sources/ProjectDescription/Cloud.swift index 247ba895487..2ea2f3267a8 100644 --- a/Sources/ProjectDescription/Cloud.swift +++ b/Sources/ProjectDescription/Cloud.swift @@ -22,13 +22,13 @@ public struct Cloud: Codable, Equatable { } /// The base URL that points to the Cloud server. - public let url: String + public var url: String /// The project unique identifier. - public let projectId: String + public var projectId: String /// The configuration options. - public let options: [Option] + public var options: [Option] /// Returns a generic cloud configuration. /// - Parameters: diff --git a/Sources/ProjectDescription/ConfigGenerationOptions.swift b/Sources/ProjectDescription/ConfigGenerationOptions.swift index 6c8c68fcc4b..c27c04fc5b2 100644 --- a/Sources/ProjectDescription/ConfigGenerationOptions.swift +++ b/Sources/ProjectDescription/ConfigGenerationOptions.swift @@ -13,31 +13,37 @@ extension Config { /// When passed, Xcode will resolve its Package Manager dependencies using the system-defined /// accounts (for example, git) instead of the Xcode-defined accounts - public let resolveDependenciesWithSystemScm: Bool + public var resolveDependenciesWithSystemScm: Bool /// Disables locking Swift packages. This can speed up generation but does increase risk if packages are not locked /// in their declarations. - public let disablePackageVersionLocking: Bool + public var disablePackageVersionLocking: Bool /// Allows setting a custom directory to be used when resolving package dependencies /// This path is passed to `xcodebuild` via the `-clonedSourcePackagesDirPath` argument - public let clonedSourcePackagesDirPath: Path? + public var clonedSourcePackagesDirPath: Path? /// Allows configuring which targets Tuist checks for potential side effects due multiple branches of the graph /// including the same static library of framework as a transitive dependency. - public let staticSideEffectsWarningTargets: StaticSideEffectsWarningTargets + public var staticSideEffectsWarningTargets: StaticSideEffectsWarningTargets + + /// The generated project has build settings and build paths modified in such a way that projects with implicit + /// dependencies won't build until all dependencies are declared explicitly. + public let enforceExplicitDependencies: Bool public static func options( resolveDependenciesWithSystemScm: Bool = false, disablePackageVersionLocking: Bool = false, clonedSourcePackagesDirPath: Path? = nil, - staticSideEffectsWarningTargets: StaticSideEffectsWarningTargets = .all + staticSideEffectsWarningTargets: StaticSideEffectsWarningTargets = .all, + enforceExplicitDependencies: Bool = false ) -> Self { self.init( resolveDependenciesWithSystemScm: resolveDependenciesWithSystemScm, disablePackageVersionLocking: disablePackageVersionLocking, clonedSourcePackagesDirPath: clonedSourcePackagesDirPath, - staticSideEffectsWarningTargets: staticSideEffectsWarningTargets + staticSideEffectsWarningTargets: staticSideEffectsWarningTargets, + enforceExplicitDependencies: enforceExplicitDependencies ) } } diff --git a/Sources/ProjectDescription/ConfigurationName.swift b/Sources/ProjectDescription/ConfigurationName.swift index 760bba77ad9..caa3d52d0ab 100644 --- a/Sources/ProjectDescription/ConfigurationName.swift +++ b/Sources/ProjectDescription/ConfigurationName.swift @@ -15,7 +15,7 @@ import Foundation /// ``` public struct ConfigurationName: ExpressibleByStringLiteral, Codable, Equatable { /// The configuration name. - public let rawValue: String + public var rawValue: String init(_ rawValue: String) { self.rawValue = rawValue diff --git a/Sources/ProjectDescription/CoreDataModel.swift b/Sources/ProjectDescription/CoreDataModel.swift index dbf40959f08..8777e46b5c7 100644 --- a/Sources/ProjectDescription/CoreDataModel.swift +++ b/Sources/ProjectDescription/CoreDataModel.swift @@ -3,10 +3,10 @@ import Foundation /// A Core Data model. public struct CoreDataModel: Codable, Equatable { /// Relative path to the model. - public let path: Path + public var path: Path /// Optional Current version (with or without extension) - public let currentVersion: String? + public var currentVersion: String? /// Creates a Core Data model from a path. /// diff --git a/Sources/ProjectDescription/Dependencies/CarthageDependencies.swift b/Sources/ProjectDescription/Dependencies/CarthageDependencies.swift index 837e644c4b3..833aa29e8d0 100644 --- a/Sources/ProjectDescription/Dependencies/CarthageDependencies.swift +++ b/Sources/ProjectDescription/Dependencies/CarthageDependencies.swift @@ -3,7 +3,7 @@ import Foundation /// A collection of Carthage dependencies. public struct CarthageDependencies: Codable, Equatable { /// List of dependencies that will be installed using Carthage. - public let dependencies: [Dependency] + public var dependencies: [Dependency] /// Creates `CarthageDependencies` instance. /// - Parameter dependencies: List of dependencies that can be installed using Carthage. diff --git a/Sources/ProjectDescription/Dependencies/PackageSettings.swift b/Sources/ProjectDescription/Dependencies/PackageSettings.swift new file mode 100644 index 00000000000..aba504b2cbd --- /dev/null +++ b/Sources/ProjectDescription/Dependencies/PackageSettings.swift @@ -0,0 +1,65 @@ +import Foundation + +/// A custom Swift Package Manager configuration +/// +/// +/// ```swift +/// // swift-tools-version: 5.8 +/// import PackageDescription +/// +/// #if TUIST +/// import ProjectDescription +/// import ProjectDescriptionHelpers +/// +/// let packageSettings = PackageSettings( +/// productTypes: [ +/// "Alamofire": .framework, // default is .staticFramework +/// ], +/// platforms: [.iOS] +/// ) +/// #endif +/// +/// let package = Package( +/// name: "PackageName", +/// dependencies: [ +/// .package(url: "https://github.com/Alamofire/Alamofire", from: "5.0.0"), +/// ] +/// ) +/// ``` +public struct PackageSettings: Codable, Equatable { + /// The custom `Product` type to be used for SPM targets. + public var productTypes: [String: Product] + + // The base settings to be used for targets generated from SwiftPackageManager + public var baseSettings: Settings + + // Additional settings to be added to targets generated from SwiftPackageManager. + public var targetSettings: [String: SettingsDictionary] + + /// Custom project configurations to be used for projects generated from SwiftPackageManager. + public var projectOptions: [String: Project.Options] + + /// The custom set of `platforms` that are used by your project + public let platforms: Set + + /// Creates `PackageSettings` instance for custom Swift Package Manager configuration. + /// - Parameter productTypes: The custom `Product` types to be used for SPM targets. + /// - Parameter baseSettings: Additional settings to be added to targets generated from SwiftPackageManager. + /// - Parameter targetSettings: Additional settings to be added to targets generated from SwiftPackageManager. + /// - Parameter projectOptions: Custom project configurations to be used for projects generated from SwiftPackageManager. + /// - Parameter platforms: The custom set of `platforms` that are used by your project + public init( + productTypes: [String: Product] = [:], + baseSettings: Settings = .settings(), + targetSettings: [String: SettingsDictionary] = [:], + projectOptions: [String: Project.Options] = [:], + platforms: Set = Set(PackagePlatform.allCases) + ) { + self.productTypes = productTypes + self.baseSettings = baseSettings + self.targetSettings = targetSettings + self.projectOptions = projectOptions + self.platforms = platforms + dumpIfNeeded(self) + } +} diff --git a/Sources/ProjectDescription/Dependencies/SwiftPackageManagerDependencies.swift b/Sources/ProjectDescription/Dependencies/SwiftPackageManagerDependencies.swift index f897ce2fe02..1513576fa3f 100644 --- a/Sources/ProjectDescription/Dependencies/SwiftPackageManagerDependencies.swift +++ b/Sources/ProjectDescription/Dependencies/SwiftPackageManagerDependencies.swift @@ -18,19 +18,19 @@ public enum PackagesOrManifest: Codable, Equatable { public struct SwiftPackageManagerDependencies: Codable, Equatable { /// The path to the `Package.swift` manifest defining the dependencies, or the list of packages that will be installed using /// Swift Package Manager. - public let packagesOrManifest: PackagesOrManifest + public var packagesOrManifest: PackagesOrManifest /// The custom `Product` type to be used for SPM targets. - public let productTypes: [String: Product] + public var productTypes: [String: Product] // The base settings to be used for targets generated from SwiftPackageManager - public let baseSettings: Settings + public var baseSettings: Settings // Additional settings to be added to targets generated from SwiftPackageManager. - public let targetSettings: [String: SettingsDictionary] + public var targetSettings: [String: SettingsDictionary] /// Custom project configurations to be used for projects generated from SwiftPackageManager. - public let projectOptions: [String: ProjectDescription.Project.Options] + public var projectOptions: [String: Project.Options] /// Creates `SwiftPackageManagerDependencies` instance. /// - Parameter packages: List of packages that will be installed using Swift Package Manager. @@ -44,7 +44,7 @@ public struct SwiftPackageManagerDependencies: Codable, Equatable { productTypes: [String: Product] = [:], baseSettings: Settings = .settings(), targetSettings: [String: SettingsDictionary] = [:], - projectOptions: [String: ProjectDescription.Project.Options] = [:] + projectOptions: [String: Project.Options] = [:] ) { packagesOrManifest = .packages(packages) self.productTypes = productTypes @@ -62,7 +62,7 @@ public struct SwiftPackageManagerDependencies: Codable, Equatable { productTypes: [String: Product] = [:], baseSettings: Settings = .settings(), targetSettings: [String: SettingsDictionary] = [:], - projectOptions: [String: ProjectDescription.Project.Options] = [:] + projectOptions: [String: Project.Options] = [:] ) { packagesOrManifest = .manifest self.productTypes = productTypes diff --git a/Sources/ProjectDescription/DeploymentTargets.swift b/Sources/ProjectDescription/DeploymentTargets.swift index 8d6c37a04b7..f9186e86940 100644 --- a/Sources/ProjectDescription/DeploymentTargets.swift +++ b/Sources/ProjectDescription/DeploymentTargets.swift @@ -5,15 +5,15 @@ import Foundation /// A struct representing the minimum deployment versions for each platform. public struct DeploymentTargets: Hashable, Codable { /// Minimum deployment version for iOS - public let iOS: String? + public var iOS: String? /// Minimum deployment version for macOS - public let macOS: String? + public var macOS: String? /// Minimum deployment version for watchOS - public let watchOS: String? + public var watchOS: String? /// Minimum deployment version for tvOS - public let tvOS: String? + public var tvOS: String? /// Minimum deployment version for visionOS - public let visionOS: String? + public var visionOS: String? public init(iOS: String? = nil, macOS: String? = nil, watchOS: String? = nil, tvOS: String? = nil, visionOS: String? = nil) { self.iOS = iOS diff --git a/Sources/ProjectDescription/Documentation.docc/Config.md b/Sources/ProjectDescription/Documentation.docc/Config.md deleted file mode 100644 index 801af65bb29..00000000000 --- a/Sources/ProjectDescription/Documentation.docc/Config.md +++ /dev/null @@ -1,12 +0,0 @@ -# ``ProjectDescription/Config`` - -## Topics - -### Related - -- ``Cache`` -- ``Cloud`` -- ``CompatibleXcodeVersions`` -- ``Plugin`` -- ``PluginLocation`` -- ``Version`` diff --git a/Sources/ProjectDescription/Documentation.docc/Dependencies.md b/Sources/ProjectDescription/Documentation.docc/Dependencies.md deleted file mode 100644 index 2c8ba04f7a3..00000000000 --- a/Sources/ProjectDescription/Documentation.docc/Dependencies.md +++ /dev/null @@ -1,11 +0,0 @@ -# ``ProjectDescription/Dependencies`` - -## Topics - -### Managing Swift Package Manager dependencies - -- ``SwiftPackageManagerDependencies`` - -### Managing Carthage dependencies - -- ``CarthageDependencies`` diff --git a/Sources/ProjectDescription/Documentation.docc/Project.md b/Sources/ProjectDescription/Documentation.docc/Project.md deleted file mode 100644 index 2ba368d4c5f..00000000000 --- a/Sources/ProjectDescription/Documentation.docc/Project.md +++ /dev/null @@ -1,18 +0,0 @@ -# ``ProjectDescription/Project`` - -## Topics - -### Configuring targets - -- ``Target`` - -### Configuring custom schemes - -- ``Scheme`` - -### Others - -- ``Options-swift.struct`` -- ``TestingOptions`` -- ``FileHeaderTemplate`` -- ``ResourceSynthesizer`` diff --git a/Sources/ProjectDescription/Documentation.docc/ProjectDescription.md b/Sources/ProjectDescription/Documentation.docc/ProjectDescription.md deleted file mode 100644 index 9f68e8edf39..00000000000 --- a/Sources/ProjectDescription/Documentation.docc/ProjectDescription.md +++ /dev/null @@ -1,37 +0,0 @@ -# ``ProjectDescription`` - -A framework that defines Tuist's manifests, like projects or workspaces. - -## Overview - -The `ProjectDescription` framework contains different types that can be used to configure your app (or framework) project structure. - -A project may consists of sources files, resources, configurations and a manifest file. -The Tuist project manifest file, called _Project.swift_, defines the project content (see ``Project`` for more). - -Multiple _Project.swift_ manifests can be collected under a Tuist workspace manifest, -called _Workspace.swift_ (see ``Workspace`` for more). - -A _Dependencies.swift_ manifests defines your external dependencies -from Swift Package Manager, Carthage or Cocoapods (see ``Dependencies`` for more). - -``Plugin`` and other configurations are supported under the _Config.swift_ manifest (see ``Config`` for more). - -## Topics - -### Defining Manifests - -- ``Project`` -- ``Workspace`` - -### Managing Dependencies - -- ``Dependencies`` - -### Plugin and Configuration - -- ``Config`` - -### Scaffolding - -- ``Template`` diff --git a/Sources/ProjectDescription/Documentation.docc/RunAction.md b/Sources/ProjectDescription/Documentation.docc/RunAction.md deleted file mode 100644 index d82a3aa3489..00000000000 --- a/Sources/ProjectDescription/Documentation.docc/RunAction.md +++ /dev/null @@ -1,8 +0,0 @@ -# ``ProjectDescription/RunAction`` - -## Topics - -### Configuration - -- ``TestActionOptions`` -- ``TestableTarget`` diff --git a/Sources/ProjectDescription/Documentation.docc/Scheme.md b/Sources/ProjectDescription/Documentation.docc/Scheme.md deleted file mode 100644 index ef9c86fcc08..00000000000 --- a/Sources/ProjectDescription/Documentation.docc/Scheme.md +++ /dev/null @@ -1,24 +0,0 @@ -# ``ProjectDescription/Scheme`` - -## Topics - -### Actions - -- ``AnalyzeAction`` -- ``ArchiveAction`` -- ``BuildAction`` -- ``ProfileAction`` -- ``RunAction`` -- ``TestAction`` -- ``ExecutionAction`` - -### Schemes configuration - -- ``LaunchArgument`` -- ``Arguments`` -- ``SchemeDiagnosticsOption`` -- ``SchemeLanguage`` -- ``Environment`` -- ``TestingOptions`` -- ``TargetReference`` - diff --git a/Sources/ProjectDescription/Documentation.docc/Settings.md b/Sources/ProjectDescription/Documentation.docc/Settings.md deleted file mode 100644 index b944e7897fe..00000000000 --- a/Sources/ProjectDescription/Documentation.docc/Settings.md +++ /dev/null @@ -1,14 +0,0 @@ -# ``ProjectDescription/Settings`` - -## Topics - -### Related - -- ``DefaultSettings`` -- ``SettingValue`` -- ``SettingsDictionary`` -- ``Configuration`` -- ``ConfigurationName`` -- ``SwiftCompilationMode`` -- ``SwiftOptimizationLevel`` -- ``DebugInformationFormat`` diff --git a/Sources/ProjectDescription/Documentation.docc/SwiftPackageManagerDependencies.md b/Sources/ProjectDescription/Documentation.docc/SwiftPackageManagerDependencies.md deleted file mode 100644 index fcf1747323f..00000000000 --- a/Sources/ProjectDescription/Documentation.docc/SwiftPackageManagerDependencies.md +++ /dev/null @@ -1,7 +0,0 @@ -# ``ProjectDescription/SwiftPackageManagerDependencies`` - -## Topics - -### Related - -- ``Package`` diff --git a/Sources/ProjectDescription/Documentation.docc/Target.md b/Sources/ProjectDescription/Documentation.docc/Target.md deleted file mode 100644 index 9c2e1bd645d..00000000000 --- a/Sources/ProjectDescription/Documentation.docc/Target.md +++ /dev/null @@ -1,44 +0,0 @@ -# ``ProjectDescription/Target`` - -## Topics - -### Defining source files - -- ``SourceFilesList`` -- ``SourceFileGlob`` - -### Defining headers - -- ``Headers`` - -### Defining resources - -- ``ResourceFileElements`` -- ``ResourceFileElement`` -- ``ResourceSynthesizer`` - -### Defining target's dependencies - -- ``TargetDependency`` - -### Configure build phases - -- ``CopyFilesAction`` -- ``TargetScript`` - -### Configuration - -- ``Destination`` -- ``Product`` -- ``DeploymentTargets`` -- ``Settings`` - -### Others - -- ``InfoPlist`` -- ``FileList`` -- ``FileListGlob`` -- ``FileElement`` -- ``CoreDataModel`` -- ``FileCodeGen`` -- ``Path`` diff --git a/Sources/ProjectDescription/Documentation.docc/TargetDependency.md b/Sources/ProjectDescription/Documentation.docc/TargetDependency.md deleted file mode 100644 index 75017fac055..00000000000 --- a/Sources/ProjectDescription/Documentation.docc/TargetDependency.md +++ /dev/null @@ -1,9 +0,0 @@ -# ``ProjectDescription/TargetDependency`` - -## Topics - -### Related - -- ``SDKStatus`` -- ``SDKType`` -- ``PlatformFilter`` diff --git a/Sources/ProjectDescription/Documentation.docc/Template.md b/Sources/ProjectDescription/Documentation.docc/Template.md deleted file mode 100644 index bf4676aad23..00000000000 --- a/Sources/ProjectDescription/Documentation.docc/Template.md +++ /dev/null @@ -1,7 +0,0 @@ -# ``ProjectDescription/Template`` - -## Topics - -### Configuration - -- ``TemplateString`` diff --git a/Sources/ProjectDescription/Documentation.docc/TestAction.md b/Sources/ProjectDescription/Documentation.docc/TestAction.md deleted file mode 100644 index 4d1e90c6402..00000000000 --- a/Sources/ProjectDescription/Documentation.docc/TestAction.md +++ /dev/null @@ -1,7 +0,0 @@ -# ``ProjectDescription/TestAction`` - -## Topics - -### Configuration - -- ``RunActionOptions`` diff --git a/Sources/ProjectDescription/EnvironmentVariable.swift b/Sources/ProjectDescription/EnvironmentVariable.swift index 850a3721c9b..57454bf81d2 100644 --- a/Sources/ProjectDescription/EnvironmentVariable.swift +++ b/Sources/ProjectDescription/EnvironmentVariable.swift @@ -5,9 +5,9 @@ public struct EnvironmentVariable: Equatable, Codable, Hashable, ExpressibleBySt // MARK: - Attributes /// The value of the environment variable - public let value: String + public var value: String /// Whether the variable is enabled or not - public let isEnabled: Bool + public var isEnabled: Bool // MARK: - Init diff --git a/Sources/ProjectDescription/ExecuteAction.swift b/Sources/ProjectDescription/ExecuteAction.swift index b9aca03db36..777fface352 100644 --- a/Sources/ProjectDescription/ExecuteAction.swift +++ b/Sources/ProjectDescription/ExecuteAction.swift @@ -2,12 +2,12 @@ import Foundation /// An action that can be executed as part of another action for pre or post execution. public struct ExecutionAction: Equatable, Codable { - public let title: String - public let scriptText: String - public let target: TargetReference? + public var title: String + public var scriptText: String + public var target: TargetReference? /// The path to the shell which shall execute this script. if it is nil, Xcode will use default value. - public let shellPath: String? + public var shellPath: String? public init(title: String = "Run Script", scriptText: String, target: TargetReference? = nil, shellPath: String? = nil) { self.title = title diff --git a/Sources/ProjectDescription/Headers.swift b/Sources/ProjectDescription/Headers.swift index 7a30cf802c4..2b240d2543f 100644 --- a/Sources/ProjectDescription/Headers.swift +++ b/Sources/ProjectDescription/Headers.swift @@ -28,19 +28,19 @@ public struct Headers: Codable, Equatable { } /// Path to an umbrella header, which will be used to get list of public headers. - public let umbrellaHeader: Path? + public var umbrellaHeader: Path? /// Relative glob pattern that points to the public headers. - public let `public`: FileList? + public var `public`: FileList? /// Relative glob pattern that points to the private headers. - public let `private`: FileList? + public var `private`: FileList? /// Relative glob pattern that points to the project headers. - public let project: FileList? + public var project: FileList? /// Rule, which determines how to resolve found duplicates in public/private/project scopes - public let exclusionRule: AutomaticExclusionRule + public var exclusionRule: AutomaticExclusionRule private init( public publicHeaders: FileList? = nil, diff --git a/Sources/ProjectDescription/LaunchArgument.swift b/Sources/ProjectDescription/LaunchArgument.swift index 6c612278106..efda1fff436 100644 --- a/Sources/ProjectDescription/LaunchArgument.swift +++ b/Sources/ProjectDescription/LaunchArgument.swift @@ -5,9 +5,9 @@ public struct LaunchArgument: Equatable, Codable { // MARK: - Attributes /// Name of argument - public let name: String + public var name: String /// If enabled then argument is marked as active - public let isEnabled: Bool + public var isEnabled: Bool // MARK: - Init diff --git a/Sources/ProjectDescription/Path.swift b/Sources/ProjectDescription/Path.swift index bb9537eecdf..f9e76787774 100644 --- a/Sources/ProjectDescription/Path.swift +++ b/Sources/ProjectDescription/Path.swift @@ -11,9 +11,9 @@ public struct Path: ExpressibleByStringInterpolation, Codable, Hashable { case relativeToRoot } - public let type: PathType - public let pathString: String - public let callerPath: String? + public var type: PathType + public var pathString: String + public var callerPath: String? /// Default PathType is `.relativeToManifest` public init(_ path: String) { diff --git a/Sources/ProjectDescription/PluginLocation.swift b/Sources/ProjectDescription/PluginLocation.swift index 1913fc9b803..a4f5d57fd97 100644 --- a/Sources/ProjectDescription/PluginLocation.swift +++ b/Sources/ProjectDescription/PluginLocation.swift @@ -3,7 +3,7 @@ import Foundation /// A location to a plugin, either local or remote. public struct PluginLocation: Codable, Equatable { /// The type of location `local` or `git`. - public let type: LocationType + public var type: LocationType /// A `Path` to a directory containing a `Plugin` manifest. /// diff --git a/Sources/ProjectDescription/ProfileAction.swift b/Sources/ProjectDescription/ProfileAction.swift index ab2bea4868e..71bb10f93cc 100644 --- a/Sources/ProjectDescription/ProfileAction.swift +++ b/Sources/ProjectDescription/ProfileAction.swift @@ -5,19 +5,19 @@ import Foundation /// It's initialized with the `.profileAction` static method public struct ProfileAction: Equatable, Codable { /// Indicates the build configuration the product should be profiled with. - public let configuration: ConfigurationName + public var configuration: ConfigurationName /// A list of actions that are executed before starting the profile process. - public let preActions: [ExecutionAction] + public var preActions: [ExecutionAction] /// A list of actions that are executed after the profile process. - public let postActions: [ExecutionAction] + public var postActions: [ExecutionAction] /// The name of the executable or target to profile. - public let executable: TargetReference? + public var executable: TargetReference? /// Command line arguments passed on launch and environment variables. - public let arguments: Arguments? + public var arguments: Arguments? init( configuration: ConfigurationName = .release, diff --git a/Sources/ProjectDescription/ProjectOptions.swift b/Sources/ProjectDescription/ProjectOptions.swift index 7ca2b59956a..8f32dc0a521 100644 --- a/Sources/ProjectDescription/ProjectOptions.swift +++ b/Sources/ProjectDescription/ProjectOptions.swift @@ -4,28 +4,28 @@ extension Project { /// Options to configure a project. public struct Options: Codable, Equatable { /// Configures automatic target schemes generation. - public let automaticSchemesOptions: AutomaticSchemesOptions + public var automaticSchemesOptions: AutomaticSchemesOptions /// Configures the default known regions - public let defaultKnownRegions: [String]? + public var defaultKnownRegions: [String]? /// Configures the development region. - public let developmentRegion: String? + public var developmentRegion: String? /// Disables generating Bundle accessors. - public let disableBundleAccessors: Bool + public var disableBundleAccessors: Bool /// Suppress logging of environment in Run Script build phases. - public let disableShowEnvironmentVarsInScriptPhases: Bool + public var disableShowEnvironmentVarsInScriptPhases: Bool /// Disable synthesized resource accessors. - public let disableSynthesizedResourceAccessors: Bool + public var disableSynthesizedResourceAccessors: Bool /// Configures text settings. - public let textSettings: TextSettings + public var textSettings: TextSettings /// Configures the name of the generated .xcodeproj. - public let xcodeProjectName: String? + public var xcodeProjectName: String? public static func options( automaticSchemesOptions: AutomaticSchemesOptions = .enabled(), @@ -91,16 +91,16 @@ extension Project.Options { /// The text settings options public struct TextSettings: Codable, Equatable { /// Whether tabs should be used instead of spaces - public let usesTabs: Bool? + public var usesTabs: Bool? /// The width of space indent - public let indentWidth: UInt? + public var indentWidth: UInt? /// The width of tab indent - public let tabWidth: UInt? + public var tabWidth: UInt? /// Whether lines should be wrapped or not - public let wrapsLines: Bool? + public var wrapsLines: Bool? public static func textSettings( usesTabs: Bool? = nil, diff --git a/Sources/ProjectDescription/ResourceFileElements.swift b/Sources/ProjectDescription/ResourceFileElements.swift index a1ff02a74a3..89ad885d4fd 100644 --- a/Sources/ProjectDescription/ResourceFileElements.swift +++ b/Sources/ProjectDescription/ResourceFileElements.swift @@ -3,7 +3,7 @@ import Foundation /// A collection of resource file. public struct ResourceFileElements: Codable, Equatable { /// List of resource file elements - public let resources: [ResourceFileElement] + public var resources: [ResourceFileElement] public init(resources: [ResourceFileElement]) { self.resources = resources diff --git a/Sources/ProjectDescription/ResourceSynthesizer.swift b/Sources/ProjectDescription/ResourceSynthesizer.swift index a48ecd22572..e6d8ecfb8ae 100644 --- a/Sources/ProjectDescription/ResourceSynthesizer.swift +++ b/Sources/ProjectDescription/ResourceSynthesizer.swift @@ -7,12 +7,12 @@ import Foundation /// - `.strings(parserOptions: ["separator": "/"])` to use strings template with SwiftGen Parser Options /// - `.strings(plugin: "MyPlugin")` to use strings template from a plugin /// - `.strings(templatePath: "Templates/Strings.stencil")` to use strings template at a given path -public struct ResourceSynthesizer: Codable, Equatable { +public struct ResourceSynthesizer: Codable, Equatable { // swiftlint:disable:this type_body_length /// Templates can be of multiple types - public let templateType: TemplateType - public let parser: Parser - public let parserOptions: [String: Parser.Option] - public let extensions: Set + public var templateType: TemplateType + public var parser: Parser + public var parserOptions: [String: Parser.Option] + public var extensions: Set /// Templates can be either a local template file, from a plugin, or a default template from tuist public enum TemplateType: Codable, Equatable { diff --git a/Sources/ProjectDescription/RunAction.swift b/Sources/ProjectDescription/RunAction.swift index a13394e187b..b30be5eea97 100644 --- a/Sources/ProjectDescription/RunAction.swift +++ b/Sources/ProjectDescription/RunAction.swift @@ -5,37 +5,37 @@ import Foundation /// It's initialized with the .runAction static method. public struct RunAction: Equatable, Codable { /// Indicates the build configuration the product should run with. - public let configuration: ConfigurationName + public var configuration: ConfigurationName /// Whether a debugger should be attached to the run process or not. - public let attachDebugger: Bool + public var attachDebugger: Bool /// The path of custom lldbinit file. - public let customLLDBInitFile: Path? + public var customLLDBInitFile: Path? /// A list of actions that are executed before starting the run process. - public let preActions: [ExecutionAction] + public var preActions: [ExecutionAction] /// A list of actions that are executed after the run process. - public let postActions: [ExecutionAction] + public var postActions: [ExecutionAction] /// The name of the executable or target to run. - public let executable: TargetReference? + public var executable: TargetReference? /// Command line arguments passed on launch and environment variables. - public let arguments: Arguments? + public var arguments: Arguments? /// List of options to set to the action. - public let options: RunActionOptions + public var options: RunActionOptions /// List of diagnostics options to set to the action. - public let diagnosticsOptions: [SchemeDiagnosticsOption] + public var diagnosticsOptions: [SchemeDiagnosticsOption] /// A target that will be used to expand the variables defined inside Environment Variables definition (e.g. $SOURCE_ROOT) - public let expandVariableFromTarget: TargetReference? + public var expandVariableFromTarget: TargetReference? /// The launch style of the action - public let launchStyle: LaunchStyle + public var launchStyle: LaunchStyle init( configuration: ConfigurationName, diff --git a/Sources/ProjectDescription/RunActionOptions.swift b/Sources/ProjectDescription/RunActionOptions.swift index e4399c6a0e2..75f3e86882e 100644 --- a/Sources/ProjectDescription/RunActionOptions.swift +++ b/Sources/ProjectDescription/RunActionOptions.swift @@ -3,21 +3,21 @@ import Foundation /// Options for the `RunAction` action public struct RunActionOptions: Equatable, Codable { /// Language to use when running the app. - public let language: SchemeLanguage? + public var language: SchemeLanguage? /// Region to use when running the app. - public let region: String? + public var region: String? /// The path of the /// [StoreKit configuration /// file](https://developer.apple.com/documentation/xcode/setting_up_storekit_testing_in_xcode#3625700). - public let storeKitConfigurationPath: Path? + public var storeKitConfigurationPath: Path? /// A simulated GPS location to use when running the app. - public let simulatedLocation: SimulatedLocation? + public var simulatedLocation: SimulatedLocation? /// Configure your project to work with the Metal frame debugger. - public let enableGPUFrameCaptureMode: GPUFrameCaptureMode + public var enableGPUFrameCaptureMode: GPUFrameCaptureMode /// Creates an `RunActionOptions` instance /// @@ -87,9 +87,9 @@ extension RunActionOptions { /// Simulated location represents a GPS location that is used when running an app on the simulator. public struct SimulatedLocation: Codable, Equatable { /// The identifier of the location (e.g. London, England) - public let identifier: String? + public var identifier: String? /// Path to a .gpx file that indicates the location - public let gpxFile: Path? + public var gpxFile: Path? private init( identifier: String? = nil, diff --git a/Sources/ProjectDescription/Scheme.swift b/Sources/ProjectDescription/Scheme.swift index 6d1811ba815..7414779cb03 100644 --- a/Sources/ProjectDescription/Scheme.swift +++ b/Sources/ProjectDescription/Scheme.swift @@ -5,24 +5,24 @@ import Foundation /// A scheme defines a collection of targets to Build, Run, Test, Profile, Analyze and Archive. public struct Scheme: Equatable, Codable { /// The name of the scheme. - public let name: String + public var name: String /// Marks the scheme as shared (i.e. one that is checked in to the repository and is visible to xcodebuild from the command /// line). - public let shared: Bool + public var shared: Bool /// When `true` the scheme doesn't show up in the dropdown scheme's list. - public let hidden: Bool + public var hidden: Bool /// Action that builds the project targets. - public let buildAction: BuildAction? + public var buildAction: BuildAction? /// Action that runs the project tests. - public let testAction: TestAction? + public var testAction: TestAction? /// Action that runs project built products. - public let runAction: RunAction? + public var runAction: RunAction? /// Action that runs the project archive. - public let archiveAction: ArchiveAction? + public var archiveAction: ArchiveAction? /// Action that profiles the project. - public let profileAction: ProfileAction? + public var profileAction: ProfileAction? /// Action that analyze the project. - public let analyzeAction: AnalyzeAction? + public var analyzeAction: AnalyzeAction? /// Creates a new instance of a scheme. /// - Parameters: diff --git a/Sources/ProjectDescription/Settings.swift b/Sources/ProjectDescription/Settings.swift index 912c6e9c19d..ff5ea547adb 100644 --- a/Sources/ProjectDescription/Settings.swift +++ b/Sources/ProjectDescription/Settings.swift @@ -38,10 +38,10 @@ public struct Configuration: Equatable, Codable { case release } - public let name: ConfigurationName - public let variant: Variant - public let settings: SettingsDictionary - public let xcconfig: Path? + public var name: ConfigurationName + public var variant: Variant + public var settings: SettingsDictionary + public var xcconfig: Path? /// Returns a debug configuration. /// @@ -116,9 +116,9 @@ extension DefaultSettings { /// A group of settings configuration. public struct Settings: Equatable, Codable { /// A dictionary with build settings that are inherited from all the configurations. - public let base: SettingsDictionary - public let configurations: [Configuration] - public let defaultSettings: DefaultSettings + public var base: SettingsDictionary + public var configurations: [Configuration] + public var defaultSettings: DefaultSettings /// Creates settings with default.configurations `Debug` and `Release` /// diff --git a/Sources/ProjectDescription/SourceFilesList.swift b/Sources/ProjectDescription/SourceFilesList.swift index 865b230bc04..2c9ab9f2300 100644 --- a/Sources/ProjectDescription/SourceFilesList.swift +++ b/Sources/ProjectDescription/SourceFilesList.swift @@ -3,19 +3,19 @@ import Foundation /// A glob pattern configuration representing source files and its compiler flags, if any. public struct SourceFileGlob: Codable, Equatable { /// Glob pattern to the source files. - public let glob: Path + public var glob: Path /// Glob patterns for source files that will be excluded. - public let excluding: [Path] + public var excluding: [Path] /// The compiler flags to be set to the source files in the sources build phase. - public let compilerFlags: String? + public var compilerFlags: String? /// The source file attribute to be set in the build phase. - public let codeGen: FileCodeGen? + public var codeGen: FileCodeGen? /// Source file condition for compilation - public let compilationCondition: PlatformCondition? + public var compilationCondition: PlatformCondition? /// Returns a source glob pattern configuration. /// @@ -68,7 +68,7 @@ extension SourceFileGlob: ExpressibleByStringInterpolation { /// A collection of source file globs. public struct SourceFilesList: Codable, Equatable { /// List glob patterns. - public let globs: [SourceFileGlob] + public var globs: [SourceFileGlob] /// Creates the source files list with the glob patterns. /// diff --git a/Sources/ProjectDescription/Target.swift b/Sources/ProjectDescription/Target.swift index 2556a10b449..a476b508f08 100644 --- a/Sources/ProjectDescription/Target.swift +++ b/Sources/ProjectDescription/Target.swift @@ -3,72 +3,72 @@ import Foundation /// A target of a project. public struct Target: Codable, Equatable { /// The name of the target. Also, the product name if not specified with ``productName``. - public let name: String + public var name: String /// The destinations this target supports, e.g. iPhone, appleVision, macCatalyst - public let destinations: Destinations + public var destinations: Destinations /// The type of build product this target will output. - public let product: Product + public var product: Product /// The built product name. If nil, it will be equal to ``name``. - public let productName: String? + public var productName: String? /// The product bundle identifier. - public let bundleId: String + public var bundleId: String /// The minimum deployment targets your product will support. - public let deploymentTargets: DeploymentTargets? + public var deploymentTargets: DeploymentTargets? /// The Info.plist representation. - public let infoPlist: InfoPlist? + public var infoPlist: InfoPlist? /// The source files of the target. /// Note: any playgrounds matched by the globs used in this property will be automatically added. - public let sources: SourceFilesList? + public var sources: SourceFilesList? /// The resource files of target. /// Note: localizable files, `*.lproj`, are supported. - public let resources: ResourceFileElements? + public var resources: ResourceFileElements? /// The build phase copy files actions for the target. - public let copyFiles: [CopyFilesAction]? + public var copyFiles: [CopyFilesAction]? /// The headers for the target. - public let headers: Headers? + public var headers: Headers? /// The entitlements representation - public let entitlements: Entitlements? + public var entitlements: Entitlements? /// The build phase scripts actions for the target. - public let scripts: [TargetScript] + public var scripts: [TargetScript] /// The target's dependencies. - public let dependencies: [TargetDependency] + public var dependencies: [TargetDependency] /// The target's settings. - public let settings: Settings? + public var settings: Settings? /// The Core Data models. - public let coreDataModels: [CoreDataModel] + public var coreDataModels: [CoreDataModel] /// The environment variables. Used by autogenerated schemes for the target. - public let environmentVariables: [String: EnvironmentVariable] + public var environmentVariables: [String: EnvironmentVariable] /// The launch arguments. Used by autogenerated schemes for the target. - public let launchArguments: [LaunchArgument] + public var launchArguments: [LaunchArgument] /// The additional files for the target. For project's additional files, see ``Project/additionalFiles``. - public let additionalFiles: [FileElement] + public var additionalFiles: [FileElement] /// The build rules used for transformation of source files during compilation. - public let buildRules: [BuildRule] + public var buildRules: [BuildRule] /// Specifies whether if the target can merge or not the dynamic dependencies as part of its binary - public let mergedBinaryType: MergedBinaryType + public var mergedBinaryType: MergedBinaryType /// Specifies whether if the target can be merged as part of another binary or not - public let mergeable: Bool + public var mergeable: Bool public init( name: String, diff --git a/Sources/ProjectDescription/TargetScript.swift b/Sources/ProjectDescription/TargetScript.swift index 076071b3ce3..71b9c86f3ec 100644 --- a/Sources/ProjectDescription/TargetScript.swift +++ b/Sources/ProjectDescription/TargetScript.swift @@ -26,37 +26,37 @@ public struct TargetScript: Codable, Equatable { // swiftlint:disable:this type_ } /// Name of the build phase when the project gets generated. - public let name: String + public var name: String /// The script that is to be executed - public let script: Script + public var script: Script /// Target script order. - public let order: Order + public var order: Order /// List of input file paths - public let inputPaths: [FileListGlob] + public var inputPaths: [FileListGlob] /// List of input filelist paths - public let inputFileListPaths: [Path] + public var inputFileListPaths: [Path] /// List of output file paths - public let outputPaths: [Path] + public var outputPaths: [Path] /// List of output filelist paths - public let outputFileListPaths: [Path] + public var outputFileListPaths: [Path] /// Whether to skip running this script in incremental builds, if nothing has changed - public let basedOnDependencyAnalysis: Bool? + public var basedOnDependencyAnalysis: Bool? /// Whether this script only runs on install builds (default is false) - public let runForInstallBuildsOnly: Bool + public var runForInstallBuildsOnly: Bool /// The path to the shell which shall execute this script. - public let shellPath: String + public var shellPath: String /// The path to the dependency file - public let dependencyFile: Path? + public var dependencyFile: Path? /// Creates the target script with its attributes. /// diff --git a/Sources/ProjectDescription/TestAction.swift b/Sources/ProjectDescription/TestAction.swift index bcc72f2d8b1..5681ec5061d 100644 --- a/Sources/ProjectDescription/TestAction.swift +++ b/Sources/ProjectDescription/TestAction.swift @@ -6,34 +6,37 @@ import Foundation /// methods respectively. public struct TestAction: Equatable, Codable { /// List of test plans. The first in the list will be the default plan. - public let testPlans: [Path]? + public var testPlans: [Path]? /// A list of testable targets, that are targets which are defined in the project with testable information. - public let targets: [TestableTarget] + public var targets: [TestableTarget] /// Command line arguments passed on launch and environment variables. - public let arguments: Arguments? + public var arguments: Arguments? /// Build configuration to run the test with. - public let configuration: ConfigurationName + public var configuration: ConfigurationName /// Whether a debugger should be attached to the test process or not. - public let attachDebugger: Bool + public var attachDebugger: Bool /// A target that will be used to expand the variables defined inside Environment Variables definition (e.g. $SOURCE_ROOT) - public let expandVariableFromTarget: TargetReference? + public var expandVariableFromTarget: TargetReference? /// A list of actions that are executed before starting the tests-run process. - public let preActions: [ExecutionAction] + public var preActions: [ExecutionAction] /// A list of actions that are executed after the tests-run process. - public let postActions: [ExecutionAction] + public var postActions: [ExecutionAction] /// List of options to set to the action. - public let options: TestActionOptions + public var options: TestActionOptions /// List of diagnostics options to set to the action. - public let diagnosticsOptions: [SchemeDiagnosticsOption] + public var diagnosticsOptions: [SchemeDiagnosticsOption] + + /// List of testIdentifiers to skip to the test + public var skippedTests: [String]? private init( testPlans: [Path]?, @@ -45,7 +48,8 @@ public struct TestAction: Equatable, Codable { preActions: [ExecutionAction], postActions: [ExecutionAction], options: TestActionOptions, - diagnosticsOptions: [SchemeDiagnosticsOption] + diagnosticsOptions: [SchemeDiagnosticsOption], + skippedTests: [String]? ) { self.testPlans = testPlans self.targets = targets @@ -57,6 +61,7 @@ public struct TestAction: Equatable, Codable { self.expandVariableFromTarget = expandVariableFromTarget self.options = options self.diagnosticsOptions = diagnosticsOptions + self.skippedTests = skippedTests } /// Returns a test action from a list of targets to be tested. @@ -81,7 +86,8 @@ public struct TestAction: Equatable, Codable { preActions: [ExecutionAction] = [], postActions: [ExecutionAction] = [], options: TestActionOptions = .options(), - diagnosticsOptions: [SchemeDiagnosticsOption] = [.mainThreadChecker] + diagnosticsOptions: [SchemeDiagnosticsOption] = [.mainThreadChecker], + skippedTests: [String] = [] ) -> Self { Self( testPlans: nil, @@ -93,7 +99,8 @@ public struct TestAction: Equatable, Codable { preActions: preActions, postActions: postActions, options: options, - diagnosticsOptions: diagnosticsOptions + diagnosticsOptions: diagnosticsOptions, + skippedTests: skippedTests ) } @@ -122,7 +129,8 @@ public struct TestAction: Equatable, Codable { preActions: preActions, postActions: postActions, options: .options(), - diagnosticsOptions: [] + diagnosticsOptions: [], + skippedTests: nil ) } } diff --git a/Sources/ProjectDescription/TestActionOptions.swift b/Sources/ProjectDescription/TestActionOptions.swift index 1672e522aaf..60762d8ab7e 100644 --- a/Sources/ProjectDescription/TestActionOptions.swift +++ b/Sources/ProjectDescription/TestActionOptions.swift @@ -3,19 +3,19 @@ import Foundation /// The type `TestActionOptions` represents a set of options for a test action. public struct TestActionOptions: Equatable, Codable { /// Language used to run the tests. - public let language: SchemeLanguage? + public var language: SchemeLanguage? /// Region used to run the tests. - public let region: String? + public var region: String? /// Preferred screen capture format for UI tests results in Xcode 15+ - public let preferredScreenCaptureFormat: ScreenCaptureFormat? + public var preferredScreenCaptureFormat: ScreenCaptureFormat? /// Whether the scheme should or not gather the test coverage data. - public let coverage: Bool + public var coverage: Bool /// A list of targets you want to gather the test coverage data for them, which are defined in the project. - public let codeCoverageTargets: [TargetReference] + public var codeCoverageTargets: [TargetReference] /// Returns a set of options for a test action. /// - Parameters: diff --git a/Sources/ProjectDescription/TestableTarget.swift b/Sources/ProjectDescription/TestableTarget.swift index be0b8849e4c..7db0498aa71 100644 --- a/Sources/ProjectDescription/TestableTarget.swift +++ b/Sources/ProjectDescription/TestableTarget.swift @@ -1,10 +1,10 @@ import Foundation public struct TestableTarget: Equatable, Codable, ExpressibleByStringInterpolation { - public let target: TargetReference - public let isSkipped: Bool - public let isParallelizable: Bool - public let isRandomExecutionOrdering: Bool + public var target: TargetReference + public var isSkipped: Bool + public var isParallelizable: Bool + public var isRandomExecutionOrdering: Bool public init( target: TargetReference, diff --git a/Sources/ProjectDescription/Version.swift b/Sources/ProjectDescription/Version.swift index 10b49244b56..3c273194f33 100644 --- a/Sources/ProjectDescription/Version.swift +++ b/Sources/ProjectDescription/Version.swift @@ -3,19 +3,19 @@ /// specifying version number requirements inside of Project.swift public struct Version: Hashable, Codable { /// The major version. - public let major: Int + public var major: Int /// The minor version. - public let minor: Int + public var minor: Int /// The patch version. - public let patch: Int + public var patch: Int /// The pre-release identifier. - public let prereleaseIdentifiers: [String] + public var prereleaseIdentifiers: [String] /// The build metadata. - public let buildMetadataIdentifiers: [String] + public var buildMetadataIdentifiers: [String] /// Create a version object. public init( diff --git a/Sources/ProjectDescription/WorkspaceGenerationOptions.swift b/Sources/ProjectDescription/WorkspaceGenerationOptions.swift index dd2e16af84d..c54dab3d865 100644 --- a/Sources/ProjectDescription/WorkspaceGenerationOptions.swift +++ b/Sources/ProjectDescription/WorkspaceGenerationOptions.swift @@ -28,20 +28,20 @@ extension Workspace { } /// Enable or disable automatic generation of schemes by Xcode. - public let enableAutomaticXcodeSchemes: Bool? + public var enableAutomaticXcodeSchemes: Bool? /// Enable or disable automatic generation of `Workspace` schemes. If enabled, options to configure code coverage and test /// targets can be passed in via associated values. - public let autogeneratedWorkspaceSchemes: AutogeneratedWorkspaceSchemes + public var autogeneratedWorkspaceSchemes: AutogeneratedWorkspaceSchemes /// Allows to suppress warnings in Xcode about updates to recommended settings added in or below the specified Xcode /// version. The warnings appear when Xcode version has been upgraded. /// It is recommended to set the version option to Xcode's version that is used for development of a project, for example /// `.lastXcodeUpgradeCheck(Version(13, 0, 0))` for Xcode 13.0.0. - public let lastXcodeUpgradeCheck: Version? + public var lastXcodeUpgradeCheck: Version? /// Allows to render markdown files inside the workspace including an .xcodesamples.plist inside it. - public let renderMarkdownReadme: Bool + public var renderMarkdownReadme: Bool public static func options( enableAutomaticXcodeSchemes: Bool? = false, diff --git a/Sources/TuistAcceptanceTesting/TuistAcceptanceFixtures.swift b/Sources/TuistAcceptanceTesting/TuistAcceptanceFixtures.swift new file mode 100644 index 00000000000..e3c842f1047 --- /dev/null +++ b/Sources/TuistAcceptanceTesting/TuistAcceptanceFixtures.swift @@ -0,0 +1,186 @@ +import Foundation + +public enum TuistAcceptanceFixtures { + case appWithBuildRules + case appWithFrameworkAndTests + case appWithPlugins + case appWithSpmDependencies + case appWithTestPlan + case commandLineToolBasic + case commandLineToolWithDynamicFramework + case commandLineToolWithDynamicLibrary + case commandLineToolWithStaticLibrary + case frameworkWithEnvironmentVariables + case frameworkWithNativeSwiftMacro + case frameworkWithSwiftMacro + case invalidManifest + case invalidWorkspaceManifestName + case iosAppLarge + case iosAppWithAppClip + case iosAppWithBuildVariables + case iosAppWithCoreData + case iosAppWithCustomConfiguration + case iosAppWithCustomDevelopmentRegion + case iosWppWithCustomResourceParserOptions + case iosAppWithCustomScheme + case iosAppWithExtensions + case iosAppWithFrameworkAndResources + case iosAppWithFrameworkAndDisabledResources + case iosAppWithFrameworkLinkingStaticFramework + case iosAppWithFrameworks + case iosAppWithHeaders + case iosAppWithHelpers + case iosAppWithImplicitDependencies + case iosAppWithIncompatibleXcode + case iosAppWithLocalBinarySwiftPackage + case iosAppWithLocalSwiftPackage + case iosAppWithMultiConfigs + case iosAppWithRemoteBinarySwiftPackage + case iosAppWithRemoteSwiftPackage + case iosAppWithStaticFrameworks + case iosAppWithStaticLibraries + case iosAppWithStaticLibraryAndPackage + case iosAppWithTemplates + case iosAppWithTests + case iosAppWithTransitiveFramework + case iosAppWithWatchapp2 + case iosAppWithXcframeworks + case iosWorkspaceWithDependencyCycle + case iosWorkspaceWithMicrofeatureArchitecture + case macosAppWithCopyFiles + case macosAppWithExtensions + case manifestWithLogs + case multiplatformAppWithExtension + case multiplatformAppWithSdk + case plugin + case projectWithFileHeaderTemplate + case projectWithInlineFileHeaderTemplate + case tuistPlugin + case visionosApp + case workspaceWithFileHeaderTemplate + case workspaceWithInlineFileHeaderTemplate + case custom(String) + + public var path: String { + switch self { + case .appWithBuildRules: + return "app_with_build_rules" + case .appWithFrameworkAndTests: + return "app_with_framework_and_tests" + case .appWithPlugins: + return "app_with_plugins" + case .appWithSpmDependencies: + return "app_with_spm_dependencies" + case .appWithTestPlan: + return "app_with_test_plan" + case .commandLineToolBasic: + return "command_line_tool_basic" + case .commandLineToolWithDynamicFramework: + return "command_line_tool_with_dynamic_framework" + case .commandLineToolWithDynamicLibrary: + return "command_line_tool_with_dynamic_library" + case .commandLineToolWithStaticLibrary: + return "command_line_tool_with_static_library" + case .frameworkWithEnvironmentVariables: + return "framework_with_environment_variables" + case .frameworkWithNativeSwiftMacro: + return "framework_with_native_swift_macro" + case .frameworkWithSwiftMacro: + return "framework_with_swift_macro" + case .invalidManifest: + return "invalid_manifest" + case .invalidWorkspaceManifestName: + return "invalid_workspace_manifest_name" + case .iosAppLarge: + return "ios_app_large" + case .iosAppWithAppClip: + return "ios_app_with_appclip" + case .iosAppWithBuildVariables: + return "ios_app_with_build_variables" + case .iosAppWithCoreData: + return "ios_app_with_coredata" + case .iosAppWithCustomConfiguration: + return "ios_app_with_custom_configuration" + case .iosAppWithCustomDevelopmentRegion: + return "ios_app_with_custom_development_region" + case .iosWppWithCustomResourceParserOptions: + return "ios_app_with_custom_resource_parser_options" + case .iosAppWithCustomScheme: + return "ios_app_with_custom_scheme" + case .iosAppWithExtensions: + return "ios_app_with_extensions" + case .iosAppWithFrameworkAndResources: + return "ios_app_with_framework_and_resources" + case .iosAppWithFrameworkAndDisabledResources: + return "ios_app_with_framework_and_disabled_resources" + case .iosAppWithFrameworkLinkingStaticFramework: + return "ios_app_with_framework_linking_static_framework" + case .iosAppWithFrameworks: + return "ios_app_with_frameworks" + case .iosAppWithHeaders: + return "ios_app_with_headers" + case .iosAppWithHelpers: + return "ios_app_with_helpers" + case .iosAppWithImplicitDependencies: + return "ios_app_with_implicit_dependencies" + case .iosAppWithIncompatibleXcode: + return "ios_app_with_incompatible_xcode" + case .iosAppWithLocalBinarySwiftPackage: + return "ios_app_with_local_binary_swift_package" + case .iosAppWithLocalSwiftPackage: + return "ios_app_with_local_swift_package" + case .iosAppWithMultiConfigs: + return "ios_app_with_multi_configs" + case .iosAppWithRemoteBinarySwiftPackage: + return "ios_app_with_remote_binary_swift_package" + case .iosAppWithRemoteSwiftPackage: + return "ios_app_with_remote_swift_package" + case .iosAppWithStaticFrameworks: + return "ios_app_with_static_frameworks" + case .iosAppWithStaticLibraries: + return "ios_app_with_static_libraries" + case .iosAppWithStaticLibraryAndPackage: + return "ios_app_with_static_library_and_package" + case .iosAppWithTemplates: + return "ios_app_with_templates" + case .iosAppWithTests: + return "ios_app_with_tests" + case .iosAppWithTransitiveFramework: + return "ios_app_with_transitive_framework" + case .iosAppWithWatchapp2: + return "ios_app_with_watchapp2" + case .iosAppWithXcframeworks: + return "ios_app_with_xcframeworks" + case .iosWorkspaceWithDependencyCycle: + return "ios_workspace_with_dependency_cycle" + case .iosWorkspaceWithMicrofeatureArchitecture: + return "ios_workspace_with_microfeature_architecture" + case .macosAppWithCopyFiles: + return "macos_app_with_copy_files" + case .macosAppWithExtensions: + return "macos_app_with_extensions" + case .manifestWithLogs: + return "manifest_with_logs" + case .multiplatformAppWithExtension: + return "multiplatform_app_with_extension" + case .multiplatformAppWithSdk: + return "multiplatform_app_with_sdk" + case .plugin: + return "plugin" + case .projectWithFileHeaderTemplate: + return "project_with_file_header_template" + case .projectWithInlineFileHeaderTemplate: + return "project_with_inline_file_header_template" + case .tuistPlugin: + return "tuist_plugin" + case .visionosApp: + return "visionos_app" + case .workspaceWithFileHeaderTemplate: + return "workspace_with_file_header_template" + case .workspaceWithInlineFileHeaderTemplate: + return "workspace_with_inline_file_header_template" + case let .custom(path): + return path + } + } +} diff --git a/Sources/TuistAcceptanceTesting/TuistAcceptanceTestCase+Extra.swift b/Sources/TuistAcceptanceTesting/TuistAcceptanceTestCase+Extra.swift new file mode 100644 index 00000000000..21f96af0ed0 --- /dev/null +++ b/Sources/TuistAcceptanceTesting/TuistAcceptanceTestCase+Extra.swift @@ -0,0 +1,81 @@ +import TSCBasic +import TuistSupport +import XcodeProj +import XCTest + +extension TuistAcceptanceTestCase { + private func headers( + for productName: String, + destination: String + ) throws -> [AbsolutePath] { + let productPath = try productPath(for: productName, destination: destination) + return FileHandler.shared.glob(productPath, glob: "**/*.h") + } + + public func productPath( + for name: String, + destination: String + ) throws -> AbsolutePath { + try XCTUnwrap( + FileHandler.shared.glob(derivedDataPath, glob: "**/Build/**/Products/\(destination)/\(name)/").first + ) + } + + public func XCTUnwrapTarget( + _ targetName: String, + in xcodeproj: XcodeProj, + file: StaticString = #file, + line: UInt = #line + ) throws -> PBXTarget { + let targets = xcodeproj.pbxproj.projects.flatMap(\.targets) + guard let target = targets.first(where: { $0.name == targetName }) + else { + XCTFail( + "Target \(targetName) doesn't exist in any of the projects' targets of the workspace", + file: file, + line: line + ) + throw XCTUnwrapError.nilValueDetected + } + + return target + } + + public func XCTAssertProductWithDestinationDoesNotContainHeaders( + _ product: String, + destination: String, + file: StaticString = #file, + line: UInt = #line + ) throws { + if try !headers(for: product, destination: destination).isEmpty { + XCTFail("Product with name \(product) and destination \(destination) contains headers", file: file, line: line) + } + } + + public func XCTAssertFrameworkEmbedded( + _ framework: String, + by targetName: String, + file: StaticString = #file, + line: UInt = #line + ) throws { + let xcodeproj = try XcodeProj(pathString: xcodeprojPath.pathString) + let target = try XCTUnwrapTarget(targetName, in: xcodeproj) + + let xcframeworkDependencies = target.embedFrameworksBuildPhases() + .filter { $0.dstSubfolderSpec == .frameworks } + .map(\.files) + .compactMap { $0 } + .flatMap { $0 } + .compactMap(\.file?.nameOrPath) + .filter { $0.contains(".framework") } + guard xcframeworkDependencies.contains("\(framework).framework") + else { + XCTFail( + "Target \(targetName) doesn't link the framework \(framework)", + file: file, + line: line + ) + return + } + } +} diff --git a/Sources/TuistAcceptanceTesting/TuistAcceptanceTestCase.swift b/Sources/TuistAcceptanceTesting/TuistAcceptanceTestCase.swift index 74aec7acb66..6c8295598e3 100644 --- a/Sources/TuistAcceptanceTesting/TuistAcceptanceTestCase.swift +++ b/Sources/TuistAcceptanceTesting/TuistAcceptanceTestCase.swift @@ -57,14 +57,14 @@ open class TuistAcceptanceTestCase: XCTestCase { try await super.tearDown() } - public func setUpFixture(_ fixture: String) throws { + public func setUpFixture(_ fixture: TuistAcceptanceFixtures) throws { let fixturesPath = sourceRootPath .appending(component: "fixtures") - fixturePath = fixtureTemporaryDirectory.path.appending(component: fixture) + fixturePath = fixtureTemporaryDirectory.path.appending(component: fixture.path) try FileHandler.shared.copy( - from: fixturesPath.appending(component: fixture), + from: fixturesPath.appending(component: fixture.path), to: fixturePath ) } @@ -78,6 +78,47 @@ open class TuistAcceptanceTestCase: XCTestCase { try await parsedCommand.run() } + public func run(_ command: RunCommand.Type, _ arguments: String...) async throws { + try await run(command, arguments) + } + + public func run(_ command: RunCommand.Type, _ arguments: [String] = []) async throws { + let arguments = [ + "--path", fixturePath.pathString, + ] + arguments + + let parsedCommand = try command.parse(arguments) + try await parsedCommand.run() + } + + public func run(_ command: EditCommand.Type, _ arguments: String...) async throws { + try await run(command, arguments) + } + + public func run(_ command: EditCommand.Type, _ arguments: [String] = []) async throws { + let arguments = arguments + [ + "--path", fixturePath.pathString, + "--permanent", + ] + + let parsedCommand = try command.parse(arguments) + try await parsedCommand.run() + + xcodeprojPath = try FileHandler.shared.contentsOfDirectory(fixturePath) + .first(where: { $0.basename == "Manifests.xcodeproj" }) + workspacePath = try FileHandler.shared.contentsOfDirectory(fixturePath) + .first(where: { $0.basename == "Manifests.xcworkspace" }) + } + + public func run(_ command: MigrationTargetsByDependenciesCommand.Type, _ arguments: String...) throws { + try run(command, arguments) + } + + public func run(_ command: MigrationTargetsByDependenciesCommand.Type, _ arguments: [String] = []) throws { + let parsedCommand = try command.parse(arguments) + try parsedCommand.run() + } + public func run(_ command: TestCommand.Type, _ arguments: [String] = []) async throws { let arguments = arguments + [ "--derived-data-path", derivedDataPath.pathString, @@ -98,6 +139,10 @@ open class TuistAcceptanceTestCase: XCTestCase { try await parsedCommand.run() } + public func run(_ command: BuildCommand.Type, _ arguments: String...) async throws { + try await run(command, arguments) + } + public func run(_ command: GenerateCommand.Type, _ arguments: [String] = []) async throws { let arguments = arguments + [ "--no-open", diff --git a/Sources/TuistAsyncQueue/AsyncQueuePersistor.swift b/Sources/TuistAsyncQueue/AsyncQueuePersistor.swift index 7404350725b..056fe00dd77 100644 --- a/Sources/TuistAsyncQueue/AsyncQueuePersistor.swift +++ b/Sources/TuistAsyncQueue/AsyncQueuePersistor.swift @@ -55,7 +55,7 @@ final class AsyncQueuePersistor: AsyncQueuePersisting { func readAll() throws -> [AsyncQueueEventTuple] { let paths = FileHandler.shared.glob(directory, glob: "*.json") var events: [AsyncQueueEventTuple] = [] - paths.forEach { eventPath in + for eventPath in paths { let fileName = eventPath.basenameWithoutExt let components = fileName.split(separator: ".") guard components.count == 3, @@ -65,7 +65,7 @@ final class AsyncQueuePersistor: AsyncQueuePersisting { /// Changing the naming convention is a breaking change. When detected /// we delete the event. try? FileHandler.shared.delete(eventPath) - return + continue } do { let data = try Data(contentsOf: eventPath.url) diff --git a/Sources/TuistAsyncQueue/Log/Logger.swift b/Sources/TuistAsyncQueue/Log/Logger.swift index bf424f36c59..a7c4210612b 100644 --- a/Sources/TuistAsyncQueue/Log/Logger.swift +++ b/Sources/TuistAsyncQueue/Log/Logger.swift @@ -1,2 +1,3 @@ import TuistSupport + let logger = Logger(label: "io.tuist.async-queue") diff --git a/Sources/TuistAutomation/Log/Logger.swift b/Sources/TuistAutomation/Log/Logger.swift index 844fcb4a39d..6b01338a82f 100644 --- a/Sources/TuistAutomation/Log/Logger.swift +++ b/Sources/TuistAutomation/Log/Logger.swift @@ -1,2 +1,3 @@ import TuistSupport + let logger = Logger(label: "io.tuist.automation") diff --git a/Sources/TuistAutomation/Utilities/TargetBuilder.swift b/Sources/TuistAutomation/Utilities/TargetBuilder.swift index fff2e958ea5..f3bffb32b51 100644 --- a/Sources/TuistAutomation/Utilities/TargetBuilder.swift +++ b/Sources/TuistAutomation/Utilities/TargetBuilder.swift @@ -18,7 +18,6 @@ public protocol TargetBuilding { /// - device: An optional device specifier to use when building the scheme. /// - osVersion: An optional OS number to use when building the scheme. /// - graphTraverser: The Graph traverser. - /// - rawXcodebuildLogs: When true, it outputs the raw xcodebuild logs. func buildTarget( _ target: GraphTarget, platform: TuistGraph.Platform, @@ -31,8 +30,7 @@ public protocol TargetBuilding { device: String?, osVersion: Version?, rosetta: Bool, - graphTraverser: GraphTraversing, - rawXcodebuildLogs: Bool + graphTraverser: GraphTraversing ) async throws } @@ -89,8 +87,7 @@ public final class TargetBuilder: TargetBuilding { device: String?, osVersion: Version?, rosetta: Bool, - graphTraverser: GraphTraversing, - rawXcodebuildLogs: Bool + graphTraverser: GraphTraversing ) async throws { logger.log(level: .notice, "Building scheme \(scheme.name)", metadata: .section) @@ -119,8 +116,7 @@ public final class TargetBuilder: TargetBuilding { rosetta: rosetta, derivedDataPath: derivedDataPath, clean: clean, - arguments: buildArguments, - rawXcodebuildLogs: rawXcodebuildLogs + arguments: buildArguments ) .printFormattedOutput() diff --git a/Sources/TuistAutomation/Utilities/TargetRunner.swift b/Sources/TuistAutomation/Utilities/TargetRunner.swift index 1784c0c3350..f950db370df 100644 --- a/Sources/TuistAutomation/Utilities/TargetRunner.swift +++ b/Sources/TuistAutomation/Utilities/TargetRunner.swift @@ -151,7 +151,7 @@ public final class TargetRunner: TargetRunning { arguments: [String] ) async throws { let settings = try await xcodeBuildController - .showBuildSettings(.workspace(workspacePath), scheme: schemeName, configuration: configuration) + .showBuildSettings(.workspace(workspacePath), scheme: schemeName, configuration: configuration, derivedDataPath: nil) let bundleId = settings[target.target.name]?.productBundleIdentifier ?? target.target.bundleId let simulator = try await simulatorController .findAvailableDevice(platform: platform, version: version, minVersion: minVersion, deviceName: deviceName) diff --git a/Sources/TuistAutomation/XcodeBuild/XcodeBuildController.swift b/Sources/TuistAutomation/XcodeBuild/XcodeBuildController.swift index cc2fe43aa75..d6ae9679203 100644 --- a/Sources/TuistAutomation/XcodeBuild/XcodeBuildController.swift +++ b/Sources/TuistAutomation/XcodeBuild/XcodeBuildController.swift @@ -5,6 +5,7 @@ import TuistCore import TuistSupport public final class XcodeBuildController: XcodeBuildControlling { + // MARK: - Attributes /// Matches lines of the forms: @@ -38,8 +39,7 @@ public final class XcodeBuildController: XcodeBuildControlling { rosetta: Bool, derivedDataPath: AbsolutePath?, clean: Bool = false, - arguments: [XcodeBuildArgument], - rawXcodebuildLogs: Bool + arguments: [XcodeBuildArgument] ) throws -> AsyncThrowingStream, Error> { var command = ["/usr/bin/xcrun", "xcodebuild"] @@ -77,7 +77,7 @@ public final class XcodeBuildController: XcodeBuildControlling { command.append(contentsOf: ["-derivedDataPath", derivedDataPath.pathString]) } - return try run(command: command, rawXcodebuildLogs: rawXcodebuildLogs) + return try run(command: command) } public func test( @@ -92,8 +92,7 @@ public final class XcodeBuildController: XcodeBuildControlling { retryCount: Int, testTargets: [TestIdentifier], skipTestTargets: [TestIdentifier], - testPlanConfiguration: TestPlanConfiguration?, - rawXcodebuildLogs: Bool + testPlanConfiguration: TestPlanConfiguration? ) throws -> AsyncThrowingStream, Error> { var command = ["/usr/bin/xcrun", "xcodebuild"] @@ -158,7 +157,7 @@ public final class XcodeBuildController: XcodeBuildControlling { } } - return try run(command: command, rawXcodebuildLogs: rawXcodebuildLogs) + return try run(command: command) } public func archive( @@ -167,7 +166,7 @@ public final class XcodeBuildController: XcodeBuildControlling { clean: Bool, archivePath: AbsolutePath, arguments: [XcodeBuildArgument], - rawXcodebuildLogs: Bool + derivedDataPath: AbsolutePath? ) throws -> AsyncThrowingStream, Error> { var command = ["/usr/bin/xcrun", "xcodebuild"] @@ -186,31 +185,27 @@ public final class XcodeBuildController: XcodeBuildControlling { // Archive path command.append(contentsOf: ["-archivePath", archivePath.pathString]) + // Derived data path + if let derivedDataPath = derivedDataPath { + command.append(contentsOf: ["-derivedDataPath", derivedDataPath.pathString]) + } + // Arguments command.append(contentsOf: arguments.flatMap(\.arguments)) - return try run(command: command, rawXcodebuildLogs: rawXcodebuildLogs) + return try run(command: command) } public func createXCFramework( - frameworks: [AbsolutePath], - output: AbsolutePath, - rawXcodebuildLogs: Bool + arguments: [XcodeBuildControllerCreateXCFrameworkArgument], + output: AbsolutePath ) throws -> AsyncThrowingStream, Error> { var command = ["/usr/bin/xcrun", "xcodebuild", "-create-xcframework"] - command.append(contentsOf: frameworks.flatMap { - let pathString = $0.pathString - return [ - "-framework", - // It's workaround for Xcode 15 RC bug - // remove it since bug will be fixed - // more details here: https://github.com/tuist/tuist/issues/5354 - pathString.hasPrefix("/var/") ? pathString.replacingOccurrences(of: "/var/", with: "/private/var/") : pathString - ] - }) + command.append(contentsOf: arguments.flatMap(\.xcodebuildArguments)) command.append(contentsOf: ["-output", output.pathString]) command.append("-allow-internal-distribution") - return try run(command: command, rawXcodebuildLogs: rawXcodebuildLogs) + + return try run(command: command) } enum ShowBuildSettingsError: Error { @@ -221,7 +216,8 @@ public final class XcodeBuildController: XcodeBuildControlling { public func showBuildSettings( _ target: XcodeBuildTarget, scheme: String, - configuration: String + configuration: String, + derivedDataPath: AbsolutePath? ) async throws -> [String: XcodeBuildSettings] { var command = ["/usr/bin/xcrun", "xcodebuild", "archive", "-showBuildSettings", "-skipUnavailableActions"] @@ -231,6 +227,11 @@ public final class XcodeBuildController: XcodeBuildControlling { // Scheme command.append(contentsOf: ["-scheme", scheme]) + // Derived data path + if let derivedDataPath = derivedDataPath { + command.append(contentsOf: ["-derivedDataPath", derivedDataPath.pathString]) + } + // Target command.append(contentsOf: target.xcodebuildArguments) @@ -289,20 +290,22 @@ public final class XcodeBuildController: XcodeBuildControlling { return buildSettingsByTargetName } - fileprivate func run(command: [String], rawXcodebuildLogs: Bool) throws -> AsyncThrowingStream, Error> { - System.shared.publisher(command) + fileprivate func run(command: [String]) throws -> AsyncThrowingStream, Error> { + + logger.debug("Running xcodebuild command: \(command.joined(separator: " "))") + return System.shared.publisher(command) .compactMap { [weak self] event -> SystemEvent? in switch event { case let .standardError(errorData): guard let line = String(data: errorData, encoding: .utf8) else { return nil } - if rawXcodebuildLogs || self?.environment.isVerbose == true { + if self?.environment.isVerbose == true { return SystemEvent.standardError(XcodeBuildOutput(raw: line)) } else { return SystemEvent.standardError(XcodeBuildOutput(raw: self?.formatter.format(line) ?? "")) } case let .standardOutput(outputData): guard let line = String(data: outputData, encoding: .utf8) else { return nil } - if rawXcodebuildLogs || self?.environment.isVerbose == true { + if self?.environment.isVerbose == true { return SystemEvent.standardOutput(XcodeBuildOutput(raw: line)) } else { return SystemEvent.standardOutput(XcodeBuildOutput(raw: self?.formatter.format(line) ?? "")) diff --git a/Sources/TuistAutomationTesting/Utilities/MockTargetBuilder.swift b/Sources/TuistAutomationTesting/Utilities/MockTargetBuilder.swift index e43bb45dc80..c5ddb03599d 100644 --- a/Sources/TuistAutomationTesting/Utilities/MockTargetBuilder.swift +++ b/Sources/TuistAutomationTesting/Utilities/MockTargetBuilder.swift @@ -18,8 +18,7 @@ public final class MockTargetBuilder: TargetBuilding { String?, Version?, Bool, - GraphTraversing, - Bool + GraphTraversing ) throws -> Void)? public func buildTarget( @@ -34,8 +33,7 @@ public final class MockTargetBuilder: TargetBuilding { device: String?, osVersion: Version?, rosetta: Bool, - graphTraverser: GraphTraversing, - rawXcodebuildLogs: Bool + graphTraverser: GraphTraversing ) throws { try buildTargetStub?( target, @@ -48,8 +46,7 @@ public final class MockTargetBuilder: TargetBuilding { device, osVersion, rosetta, - graphTraverser, - rawXcodebuildLogs + graphTraverser ) } } diff --git a/Sources/TuistCore/Automation/XcodeBuildArgument.swift b/Sources/TuistCore/Automation/XcodeBuildArgument.swift index f2fb9631afb..c360e4de78a 100644 --- a/Sources/TuistCore/Automation/XcodeBuildArgument.swift +++ b/Sources/TuistCore/Automation/XcodeBuildArgument.swift @@ -19,6 +19,9 @@ public enum XcodeBuildArgument: Equatable, CustomStringConvertible { /// Specifies number of retry attempts on failure for testing case retryCount(Int) + /// Specifies architecture for which to build + case arch(String) + /// To pass additional arguments case xcarg(String, String) @@ -35,6 +38,8 @@ public enum XcodeBuildArgument: Equatable, CustomStringConvertible { return ["-derivedDataPath", path.pathString] case let .retryCount(count): return ["-retry-tests-on-failure", "-test-iterations", "\(count + 1)"] + case let .arch(architecture): + return ["-arch", architecture] case let .xcarg(key, value): return ["\(key)=\(value.spm_shellEscaped())"] } @@ -53,6 +58,8 @@ public enum XcodeBuildArgument: Equatable, CustomStringConvertible { return "Xcodebuild's derivedDataPath argument: \(path.pathString)" case let .retryCount(count): return "Xcodebuild's retry-tests-on-failure argument combined with test-iterations: \(count + 1)" + case let .arch(architecture): + return "Xcodebuild's arch argument: \(architecture)" case let .xcarg(key, value): return "Xcodebuild's additional argument: \(key)=\(value)" } diff --git a/Sources/TuistCore/Automation/XcodeBuildControlling.swift b/Sources/TuistCore/Automation/XcodeBuildControlling.swift index 1b38b704d5a..f30affcb1af 100644 --- a/Sources/TuistCore/Automation/XcodeBuildControlling.swift +++ b/Sources/TuistCore/Automation/XcodeBuildControlling.swift @@ -7,6 +7,61 @@ public enum XcodeBuildDestination: Equatable { case mac } +/// An enum that represents value pairs that can be passed when creating an .xcframework. +public enum XcodeBuildControllerCreateXCFrameworkArgument { + /** + An argument that represents a framework archive. The argument is a tuple containing the + absolute path to the archive, and the name of the framework inside the archive. + + xcodebuild -create-xcframework + -archive archives/MyFramework-iOS.xcarchive -framework MyFramework.framework + -archive archives/MyFramework-iOS_Simulator.xcarchive -framework MyFramework.framework + -archive archives/MyFramework-macOS.xcarchive -framework MyFramework.framework + -archive archives/MyFramework-Mac_Catalyst.xcarchive -framework MyFramework.framework + -output xcframeworks/MyFramework.xcframework + */ + case framework(archivePath: AbsolutePath, framework: String) + + /** + An argument that represents a library. The argument is a tuple containing the absolute path + to the library, and the absolute path to the directory containing the headers. + + xcodebuild -create-xcframework + -library products/iOS/usr/local/lib/libMyLibrary.a -headers products/iOS/usr/local/include + -library products/iOS_Simulator/usr/local/lib/libMyLibrary.a -headers products/iOS/usr/local/include + -library products/macOS/usr/local/lib/libMyLibrary.a -headers products/macOS/usr/local/include + -library products/Mac\ Catalyst/usr/local/lib/libMyLibrary.a -headers products/Mac\ Catalyst/usr/local/include + -output xcframeworks/MyLibrary.xcframework + */ + case library(path: AbsolutePath, headers: AbsolutePath) + + /** + It passes the -debug-symbol argument when creating frameworks. + */ + case debugSymbols(path: AbsolutePath) + + /** + Returns the arguments that represent his argument when invoking xcodebuild. + */ + public var xcodebuildArguments: [String] { + func sanitizedPath(_ path: AbsolutePath) -> String { + // It's workaround for Xcode 15 RC bug + // remove it since bug will be fixed + // more details here: https://github.com/tuist/tuist/issues/5354 + path.pathString.hasPrefix("/var/") ? path.pathString.replacingOccurrences(of: "/var/", with: "/private/var/") : path + .pathString + } + switch self { + case let .framework(archivePath, framework): + return ["-archive", sanitizedPath(archivePath), "-framework", framework] + case let .library(libraryPath, headers): + return ["-library", sanitizedPath(libraryPath), "-headers", sanitizedPath(headers)] + case let .debugSymbols(path): + return ["-debug-symbols", sanitizedPath(path)] + } + } +} + public protocol XcodeBuildControlling { /// Returns an observable to build the given project using xcodebuild. /// - Parameters: @@ -16,7 +71,6 @@ public protocol XcodeBuildControlling { /// to determine the destination. /// - clean: True if xcodebuild should clean the project before building. /// - arguments: Extra xcodebuild arguments. - /// - rawXcodebuildLogs: When true, it outputs the raw xcodebuild logs. func build( _ target: XcodeBuildTarget, scheme: String, @@ -24,8 +78,7 @@ public protocol XcodeBuildControlling { rosetta: Bool, derivedDataPath: AbsolutePath?, clean: Bool, - arguments: [XcodeBuildArgument], - rawXcodebuildLogs: Bool + arguments: [XcodeBuildArgument] ) throws -> AsyncThrowingStream, Error> /// Returns an observable to test the given project using xcodebuild. @@ -40,7 +93,6 @@ public protocol XcodeBuildControlling { /// - testTargets: A list of test identifiers indicating which tests to run /// - skipTestTargets: A list of test identifiers indicating which tests to skip /// - testPlanConfiguration: A configuration object indicating which test plan to use and its configurations - /// - rawXcodebuildLogs: When true, it outputs the raw xcodebuild logs. func test( _ target: XcodeBuildTarget, scheme: String, @@ -53,8 +105,7 @@ public protocol XcodeBuildControlling { retryCount: Int, testTargets: [TestIdentifier], skipTestTargets: [TestIdentifier], - testPlanConfiguration: TestPlanConfiguration?, - rawXcodebuildLogs: Bool + testPlanConfiguration: TestPlanConfiguration? ) throws -> AsyncThrowingStream, Error> /// Returns an observable that archives the given project using xcodebuild. @@ -64,22 +115,24 @@ public protocol XcodeBuildControlling { /// - clean: True if xcodebuild should clean the project before archiving. /// - archivePath: Path where the archive will be exported (with extension .xcarchive) /// - arguments: Extra xcodebuild arguments. - /// - rawXcodebuildLogs: When true, it outputs the raw xcodebuild logs. + /// - derivedDataPath: Custom location for derived data. Use `xcodebuild`'s default if `nil` func archive( _ target: XcodeBuildTarget, scheme: String, clean: Bool, archivePath: AbsolutePath, arguments: [XcodeBuildArgument], - rawXcodebuildLogs: Bool + derivedDataPath: AbsolutePath? ) throws -> AsyncThrowingStream, Error> /// Creates an .xcframework combining the list of given frameworks. /// - Parameters: - /// - frameworks: Frameworks to be combined. + /// - arguments: A set of arguments to configure the XCFramework creation. /// - output: Path to the output .xcframework. - /// - rawXcodebuildLogs: When true, it outputs the raw xcodebuild logs. - func createXCFramework(frameworks: [AbsolutePath], output: AbsolutePath, rawXcodebuildLogs: Bool) + func createXCFramework( + arguments: [XcodeBuildControllerCreateXCFrameworkArgument], + output: AbsolutePath + ) throws -> AsyncThrowingStream, Error> /// Gets the build settings of a scheme targets. @@ -87,9 +140,11 @@ public protocol XcodeBuildControlling { /// - target: Project of workspace where the scheme is defined. /// - scheme: Scheme whose target build settings will be obtained. /// - configuration: Build configuration. + /// - derivedDataPath: Custom location for derived data. Use `xcodebuild`'s default if `nil` func showBuildSettings( _ target: XcodeBuildTarget, scheme: String, - configuration: String + configuration: String, + derivedDataPath: AbsolutePath? ) async throws -> [String: XcodeBuildSettings] } diff --git a/Sources/TuistCore/Cache/CacheCategory.swift b/Sources/TuistCore/Cache/CacheCategory.swift index 850bc9636b8..96017a4f602 100644 --- a/Sources/TuistCore/Cache/CacheCategory.swift +++ b/Sources/TuistCore/Cache/CacheCategory.swift @@ -25,7 +25,7 @@ public enum CacheCategory: String, CaseIterable, RawRepresentable { case .builds: return "BuildCache" case .tests: - return "TestsCache" + return "incremental-tests" case .generatedAutomationProjects: return "Projects" case .projectDescriptionHelpers: diff --git a/Sources/TuistCore/Graph/CircularDependencyLinter.swift b/Sources/TuistCore/Graph/CircularDependencyLinter.swift index 1a564f5f694..31810672805 100644 --- a/Sources/TuistCore/Graph/CircularDependencyLinter.swift +++ b/Sources/TuistCore/Graph/CircularDependencyLinter.swift @@ -16,9 +16,9 @@ public class CircularDependencyLinter: CircularDependencyLinting { public func lintWorkspace(workspace: Workspace, projects: [Project]) throws { let cycleDetector = GraphCircularDetector() let cache = GraphLoader.Cache(projects: projects) - try workspace.projects.forEach { + for project in workspace.projects { try lintProject( - path: $0, + path: project, cache: cache, cycleDetector: cycleDetector ) @@ -40,10 +40,10 @@ public class CircularDependencyLinter: CircularDependencyLinting { } cache.add(project: project) - try project.targets.forEach { + for target in project.targets { try lintTarget( path: path, - name: $0.name, + name: target.name, cache: cache, cycleDetector: cycleDetector ) @@ -70,11 +70,11 @@ public class CircularDependencyLinter: CircularDependencyLinting { cache.add(target: target, path: path) - try target.dependencies.forEach { + for item in target.dependencies { try lintDependency( path: path, fromTarget: target.name, - dependency: $0, + dependency: item, cache: cache, cycleDetector: cycleDetector ) diff --git a/Sources/TuistCore/Graph/ConditionCache.swift b/Sources/TuistCore/Graph/ConditionCache.swift new file mode 100644 index 00000000000..5a7e0d8b81d --- /dev/null +++ b/Sources/TuistCore/Graph/ConditionCache.swift @@ -0,0 +1,50 @@ +import Foundation +import TuistGraph +import TuistSupport + +/// Cache designed to store `PlatformCondition.CombinationResult` for `GraphTraverser` +final class ConditionCache { + private final class ConditionCacheValue { + let conditionResult: PlatformCondition.CombinationResult + init(conditionResult: PlatformCondition.CombinationResult) { + self.conditionResult = conditionResult + } + } + + private final class GraphEdgeCacheKey: NSObject { + let edge: GraphEdge + init(_ edge: GraphEdge) { + self.edge = edge + } + + override func isEqual(_ object: Any?) -> Bool { + guard let other = object as? Self else { + return false + } + + return edge == other.edge + } + + override var hash: Int { + edge.hashValue + } + } + + private let cache = NSCache() + + public subscript(_ edge: (GraphDependency, GraphDependency)) -> PlatformCondition.CombinationResult? { + get { + cache.object(forKey: GraphEdgeCacheKey(GraphEdge(from: edge.0, to: edge.1)))?.conditionResult + } + set { + if let newValue { + cache.setObject( + ConditionCacheValue(conditionResult: newValue), + forKey: GraphEdgeCacheKey(GraphEdge(from: edge.0, to: edge.1)) + ) + } else { + cache.removeObject(forKey: GraphEdgeCacheKey(GraphEdge(from: edge.0, to: edge.1))) + } + } + } +} diff --git a/Sources/TuistCore/Graph/GraphDependencyReference.swift b/Sources/TuistCore/Graph/GraphDependencyReference.swift index 5eed93602ca..60eb1a71d37 100644 --- a/Sources/TuistCore/Graph/GraphDependencyReference.swift +++ b/Sources/TuistCore/Graph/GraphDependencyReference.swift @@ -12,9 +12,12 @@ public enum GraphDependencyReference: Equatable, Comparable, Hashable { let .product(_, _, condition), let .sdk(_, _, _, condition): return condition + case .macro: + return nil } } + case macro(path: AbsolutePath) case xcframework( path: AbsolutePath, infoPlist: XCFrameworkInfoPlist, @@ -69,13 +72,13 @@ public enum GraphDependencyReference: Equatable, Comparable, Hashable { product: (linking == .static) ? .staticLibrary : .dynamicLibrary, condition: condition ) - case let .xcframework(path, infoPlist, primaryBinaryPath, _, _, status): + case let .xcframework(xcframework): self = .xcframework( - path: path, - infoPlist: infoPlist, - primaryBinaryPath: primaryBinaryPath, - binaryPath: primaryBinaryPath, - status: status, + path: xcframework.path, + infoPlist: xcframework.infoPlist, + primaryBinaryPath: xcframework.primaryBinaryPath, + binaryPath: xcframework.primaryBinaryPath, + status: xcframework.status, condition: condition ) default: @@ -109,6 +112,7 @@ public enum GraphDependencyReference: Equatable, Comparable, Hashable { // Private helper type to auto-synthesize the hashable & comparable implementations // where only the required subset of properties are used. private enum Synthesized: Comparable, Hashable { + case macro(path: AbsolutePath) case sdk(path: AbsolutePath, condition: PlatformCondition?) case product(target: String, productName: String, condition: PlatformCondition?) case library(path: AbsolutePath, condition: PlatformCondition?) @@ -118,6 +122,8 @@ public enum GraphDependencyReference: Equatable, Comparable, Hashable { init(dependencyReference: GraphDependencyReference) { switch dependencyReference { + case let .macro(path): + self = .macro(path: path) case .xcframework( path: let path, infoPlist: _, diff --git a/Sources/TuistCore/Graph/GraphLoader.swift b/Sources/TuistCore/Graph/GraphLoader.swift index ee16da61e5f..da0c2e97799 100644 --- a/Sources/TuistCore/Graph/GraphLoader.swift +++ b/Sources/TuistCore/Graph/GraphLoader.swift @@ -41,9 +41,9 @@ public final class GraphLoader: GraphLoading { public func loadWorkspace(workspace: Workspace, projects: [Project]) throws -> Graph { let cache = Cache(projects: projects) - try workspace.projects.forEach { + for project in workspace.projects { try loadProject( - path: $0, + path: project, cache: cache ) } @@ -76,10 +76,10 @@ public final class GraphLoader: GraphLoading { } cache.add(project: project) - try project.targets.forEach { + for target in project.targets { try loadTarget( path: path, - name: $0.name, + name: target.name, cache: cache ) } @@ -120,6 +120,7 @@ public final class GraphLoader: GraphLoading { } } + // swiftlint:disable:next function_body_length private func loadDependency( path: AbsolutePath, forPlatforms platforms: Set, @@ -259,14 +260,15 @@ public final class GraphLoader: GraphLoading { at: path, status: status ) - let xcframework: GraphDependency = .xcframework( + let xcframework: GraphDependency = .xcframework(GraphDependency.XCFramework( path: metadata.path, infoPlist: metadata.infoPlist, primaryBinaryPath: metadata.primaryBinaryPath, linking: metadata.linking, mergeable: metadata.mergeable, - status: metadata.status - ) + status: metadata.status, + macroPath: metadata.macroPath + )) cache.add(xcframework: xcframework, at: path) return xcframework } @@ -330,8 +332,8 @@ public final class GraphLoader: GraphLoading { func add(project: Project) { loadedProjects[project.path] = project - project.packages.forEach { - packages[project.path, default: [:]][$0.name] = $0 + for package in project.packages { + packages[project.path, default: [:]][package.name] = package } } diff --git a/Sources/TuistCore/Graph/GraphTraverser.swift b/Sources/TuistCore/Graph/GraphTraverser.swift index 940c0c6b808..91bb0717aab 100644 --- a/Sources/TuistCore/Graph/GraphTraverser.swift +++ b/Sources/TuistCore/Graph/GraphTraverser.swift @@ -14,6 +14,7 @@ public class GraphTraverser: GraphTraversing { public var dependencies: [GraphDependency: Set] { graph.dependencies } private let graph: Graph + private let conditionCache = ConditionCache() private let systemFrameworkMetadataProvider: SystemFrameworkMetadataProviding = SystemFrameworkMetadataProvider() public required init(graph: Graph) { @@ -43,7 +44,7 @@ public class GraphTraverser: GraphTraversing { try topologicalSort( Array(allTargets()), successors: { - Array(directTargetDependencies(path: $0.path, name: $0.target.name)) + directTargetDependencies(path: $0.path, name: $0.target.name).map(\.graphTarget) } ).reversed() } @@ -79,7 +80,7 @@ public class GraphTraverser: GraphTraversing { public func targets(product: Product) -> Set { var filteredTargets: Set = Set() - targets.forEach { path, projectTargets in + for (path, projectTargets) in targets { projectTargets.values.forEach { target in guard target.product == product else { return } guard let project = projects[path] else { return } @@ -104,54 +105,56 @@ public class GraphTraverser: GraphTraversing { allTestPlans().first { $0.name == name } } - public func directTargetDependencies(path: AbsolutePath, name: String) -> Set { - guard let dependencies = graph.dependencies[.target(name: name, path: path)] + public func allTargetDependencies(path: AbsolutePath, name: String) -> Set { + guard let target = target(path: path, name: name) else { return [] } + return transitiveClosure([target]) { target in + Array( + directTargetDependencies( + path: target.path, + name: target.target.name + ) + ).map(\.graphTarget) + } + } + + public func directTargetDependencies(path: AbsolutePath, name: String) -> Set { + let target = GraphDependency.target(name: name, path: path) + guard let dependencies = graph.dependencies[target] else { return [] } let targetDependencies = dependencies .compactMap(\.targetDependency) - return Set(targetDependencies.flatMap { dependencyName, dependencyPath -> [GraphTarget] in - guard let projectDependencies = graph.targets[dependencyPath], - let dependencyTarget = projectDependencies[dependencyName], - let dependencyProject = graph.projects[dependencyPath] - else { - return [] - } - return [GraphTarget(path: dependencyPath, target: dependencyTarget, project: dependencyProject)] - }) + return Set(convertToGraphTargetReferences(targetDependencies, for: target)) } - public func directLocalTargetDependencies(path: AbsolutePath, name: String) -> Set { - guard let dependencies = graph.dependencies[.target(name: name, path: path)] else { return [] } - guard let project = graph.projects[path] else { return Set() } + public func directLocalTargetDependencies(path: AbsolutePath, name: String) -> Set { + let target = GraphDependency.target(name: name, path: path) + guard let dependencies = graph.dependencies[target], + graph.projects[path] != nil + else { return [] } let localTargetDependencies = dependencies .compactMap(\.targetDependency) .filter { $0.path == path } - return Set(localTargetDependencies.flatMap { dependencyName, dependencyPath -> [GraphTarget] in + + return Set(convertToGraphTargetReferences(localTargetDependencies, for: target)) + } + + func convertToGraphTargetReferences( + _ dependencies: [(name: String, path: AbsolutePath)], + for target: GraphDependency + ) -> [GraphTargetReference] { + dependencies.compactMap { dependencyName, dependencyPath -> GraphTargetReference? in guard let projectDependencies = graph.targets[dependencyPath], - let dependencyTarget = projectDependencies[dependencyName] + let dependencyTarget = projectDependencies[dependencyName], + let dependencyProject = graph.projects[dependencyPath] else { - return [] + return nil } - return [GraphTarget(path: path, target: dependencyTarget, project: project)] - }) - } - - public func directLocalTargetDependenciesWithConditions(path: AbsolutePath, name: String) -> [( - GraphTarget, - PlatformCondition? - )] { - let sorted = directLocalTargetDependencies(path: path, name: name).sorted() - let from = GraphDependency.target(name: name, path: path) - - return sorted.map { dependency in - let condition = graph.dependencyConditions[GraphEdge( - from: from, - to: GraphDependency.target(name: dependency.target.name, path: dependency.path) - )] - return (dependency, condition) + let condition = graph.dependencyConditions[(target, .target(name: dependencyTarget.name, path: dependencyPath))] + let graphTarget = GraphTarget(path: dependencyPath, target: dependencyTarget, project: dependencyProject) + return GraphTargetReference(target: graphTarget, condition: condition) } } @@ -181,7 +184,7 @@ public class GraphTraverser: GraphTraversing { return GraphTarget(path: path, target: target, project: project) } - public func appExtensionDependencies(path: AbsolutePath, name: String) -> Set { + public func appExtensionDependencies(path: AbsolutePath, name: String) -> Set { let validProducts: [Product] = [ .appExtension, .stickerPackExtension, .watch2Extension, .tvTopShelfExtension, .messagesExtension, ] @@ -191,7 +194,7 @@ public class GraphTraverser: GraphTraversing { ) } - public func extensionKitExtensionDependencies(path: TSCBasic.AbsolutePath, name: String) -> Set { + public func extensionKitExtensionDependencies(path: TSCBasic.AbsolutePath, name: String) -> Set { let validProducts: [Product] = [ .extensionKitExtension, ] @@ -201,7 +204,7 @@ public class GraphTraverser: GraphTraversing { ) } - public func appClipDependencies(path: AbsolutePath, name: String) -> GraphTarget? { + public func appClipDependencies(path: AbsolutePath, name: String) -> GraphTargetReference? { directLocalTargetDependencies(path: path, name: name) .first { $0.target.product == .appClip } } @@ -250,8 +253,8 @@ public class GraphTraverser: GraphTraversing { /// Precompiled frameworks var precompiledFrameworks = filterDependencies( from: .target(name: name, path: path), - test: isDependencyPrecompiledDynamicAndLinkable, - skip: canDependencyEmbedProducts + test: { $0.isPrecompiledDynamicAndLinkable }, + skip: or(canDependencyEmbedProducts, isDependencyPrecompiledMacro) ) // Skip merged precompiled libraries from merging into the runnable binary if case let .manual(dependenciesToMerge) = target.target.mergedBinaryType { @@ -355,10 +358,11 @@ public class GraphTraverser: GraphTraversing { .filter(\.isPrecompiled) let precompiledDependencies = precompiled - .flatMap { filterDependencies(from: $0) } + .flatMap { filterDependencies(from: $0) + } let precompiledLibrariesAndFrameworks = Set(precompiled + precompiledDependencies) - .filter(isDependencyPrecompiledDynamicAndLinkable) + .filter(\.isPrecompiledDynamicAndLinkable) .compactMap { dependencyReference(to: $0, from: targetGraphDependency) } references.formUnion(precompiledLibrariesAndFrameworks) @@ -389,7 +393,7 @@ public class GraphTraverser: GraphTraversing { let staticDependenciesPrecompiledLibrariesAndFrameworks = transitiveStaticTargetReferences.flatMap { dependency in self.graph.dependencies[dependency, default: []] .lazy - .filter(\.isPrecompiled) + .filter { $0.isPrecompiled && $0.isLinkable } } let allDependencies = ( @@ -445,9 +449,16 @@ public class GraphTraverser: GraphTraversing { return Set(dependencies) } - public func directSwiftMacroFrameworkTargets(path: AbsolutePath, name: String) -> Set { + public func directSwiftMacroTargets(path: AbsolutePath, name: String) -> Set { let dependencies = directTargetDependencies(path: path, name: name) - .filter { $0.target.product == .staticFramework } + .filter { [.staticFramework, .framework, .dynamicLibrary, .staticLibrary].contains($0.target.product) } + .filter { self.directSwiftMacroExecutables(path: $0.graphTarget.path, name: $0.graphTarget.target.name).count != 0 } + return Set(dependencies) + } + + public func allSwiftMacroTargets(path: AbsolutePath, name: String) -> Set { + let dependencies = allTargetDependencies(path: path, name: name) + .filter { [.staticFramework, .framework, .dynamicLibrary, .staticLibrary].contains($0.target.product) } .filter { self.directSwiftMacroExecutables(path: $0.path, name: $0.target.name).count != 0 } return Set(dependencies) } @@ -509,14 +520,15 @@ public class GraphTraverser: GraphTraversing { let from = GraphDependency.target(name: name, path: path) let precompiledFrameworksPaths = filterDependencies( from: from, - test: isDependencyPrecompiledDynamicAndLinkable, + test: { $0.isPrecompiledDynamicAndLinkable }, skip: canDependencyEmbedProducts ) .lazy .compactMap { (dependency: GraphDependency) -> AbsolutePath? in switch dependency { - case let .xcframework(path, _, _, _, _, _): return path + case let .xcframework(xcframework): return xcframework.path case let .framework(path, _, _, _, _, _, _, _): return path + case .macro: return nil case .library: return nil case .bundle: return nil case .packageProduct: return nil @@ -552,10 +564,10 @@ public class GraphTraverser: GraphTraversing { var references: Set = Set() // Linkable dependencies - try targets.forEach { target in - try references.formUnion(self.linkableDependencies(path: path, name: target.target.name)) - references.formUnion(self.embeddableFrameworks(path: path, name: target.target.name)) - references.formUnion(self.copyProductDependencies(path: path, name: target.target.name)) + for target in targets { + try references.formUnion(linkableDependencies(path: path, name: target.target.name)) + references.formUnion(embeddableFrameworks(path: path, name: target.target.name)) + references.formUnion(copyProductDependencies(path: path, name: target.target.name)) } return references } @@ -590,6 +602,103 @@ public class GraphTraverser: GraphTraversing { ) } + public func targetsWithExternalDependencies() -> Set { + allInternalTargets().filter { directTargetExternalDependencies(path: $0.path, name: $0.target.name).count != 0 } + } + + public func directTargetExternalDependencies(path: AbsolutePath, name: String) -> Set { + directTargetDependencies(path: path, name: name).filter(\.graphTarget.project.isExternal) + } + + public func allExternalTargets() -> Set { + Set(graph.projects.compactMap { path, project in + project.isExternal ? (path, project) : nil + }.flatMap { projectPath, project in + let targets = graph.targets[projectPath, default: [:]].values + return targets.map { GraphTarget(path: projectPath, target: $0, project: project) } + }) + } + + public func allOrphanExternalTargets() -> Set { + let graphDependenciesWithExternalDependencies = Set( + targetsWithExternalDependencies() + .map { GraphDependency.target(name: $0.target.name, path: $0.project.path) } + ) + + let allTargetExternalDependendedUponTargets = filterDependencies(from: graphDependenciesWithExternalDependencies) + .compactMap { graphDependency -> GraphTarget? in + if case let GraphDependency.target(name, path) = graphDependency { + let target = graph.targets[path]![name]! + let project = graph.projects[path]! + return GraphTarget(path: path, target: target, project: project) + } else { + return nil + } + } + let allExternalTargets = allExternalTargets() + return allExternalTargets.subtracting(allTargetExternalDependendedUponTargets) + } + + public func allSwiftPluginExecutables(path: TSCBasic.AbsolutePath, name: String) -> Set { + func precompiledMacroDependencies(_ graphDependency: GraphDependency) -> Set { + Set( + dependencies[graphDependency, default: Set()] + .lazy + .compactMap { + if case let GraphDependency.macro(path) = $0 { + return path + } else { + return nil + } + } + ) + } + + let precompiledMacroPluginExecutables = filterDependencies(from: .target(name: name, path: path), test: { dependency in + switch dependency { + case .xcframework: + return !precompiledMacroDependencies(dependency).isEmpty + case .macro: + return true + case .bundle, .library, .framework, .sdk, .target, .packageProduct: + return false + } + }, skip: { dependency in + switch dependency { + case .macro: + return true + case .bundle, .library, .framework, .sdk, .target, .packageProduct, .xcframework: + return false + } + }) + .flatMap { dependency in + switch dependency { + case .xcframework: + return Array(precompiledMacroDependencies(dependency)) + case let .macro(path): + return [path] + case .bundle, .library, .framework, .sdk, .target, .packageProduct: + return [] + } + } + .map { "\($0.pathString)#\($0.basename.replacingOccurrences(of: ".macro", with: ""))" } + + let sourceMacroPluginExecutables = allSwiftMacroTargets(path: path, name: name) + .flatMap { target in + directSwiftMacroExecutables(path: target.project.path, name: target.target.name).map { (target, $0) } + } + .compactMap { _, dependencyReference in + switch dependencyReference { + case let .product(_, productName, _): + return "$BUILD_DIR/Debug$EFFECTIVE_PLATFORM_NAME/\(productName)#\(productName)" + default: + return nil + } + } + + return Set(precompiledMacroPluginExecutables + sourceMacroPluginExecutables) + } + // MARK: - Internal /// The method collects the dependencies that are selected by the provided test closure. @@ -602,10 +711,24 @@ public class GraphTraverser: GraphTraversing { from rootDependency: GraphDependency, test: (GraphDependency) -> Bool = { _ in true }, skip: (GraphDependency) -> Bool = { _ in false } + ) -> Set { + filterDependencies(from: [rootDependency], test: test, skip: skip) + } + + /// The method collects the dependencies that are selected by the provided test closure. + /// The skip closure allows skipping the traversing of a specific dependendency branch. + /// - Parameters: + /// - from: Dependencies from which the traverse is done. + /// - test: If the closure returns true, the dependency is included. + /// - skip: If the closure returns false, the traversing logic doesn't traverse the dependencies from that dependency. + func filterDependencies( + from rootDependencies: Set, + test: (GraphDependency) -> Bool = { _ in true }, + skip: (GraphDependency) -> Bool = { _ in false } ) -> Set { var stack = Stack() - stack.push(rootDependency) + stack.push(Array(rootDependencies)) var visited: Set = .init() var references = Set() @@ -621,11 +744,11 @@ public class GraphTraverser: GraphTraversing { visited.insert(node) - if node != rootDependency, test(node) { + if !rootDependencies.contains(node), test(node) { references.insert(node) } - if node != rootDependency, skip(node) { + if !rootDependencies.contains(node), skip(node) { continue } @@ -635,7 +758,6 @@ public class GraphTraverser: GraphTraversing { } } } - return references } @@ -643,53 +765,104 @@ public class GraphTraverser: GraphTraversing { /// - Parameters: /// - rootDependency: dependency whose platform filters we need when depending on `transitiveDependency` /// - transitiveDependency: target dependency - /// - Returns: CombinationResult which represents a resolved condition or `.invalid` based on traversing - func combinedCondition(to transitiveDependency: GraphDependency, from rootDependency: GraphDependency) -> PlatformCondition + /// - Returns: CombinationResult which represents a resolved condition or `.incompatible` based on traversing + public func combinedCondition( + to transitiveDependency: GraphDependency, + from rootDependency: GraphDependency + ) -> PlatformCondition .CombinationResult { - var visited: Set = [] + if let cached = conditionCache[(rootDependency, transitiveDependency)] { + return cached + } else if graph.dependencyConditions.isEmpty { + return .condition(nil) + } - func find(from root: GraphDependency, to other: GraphDependency) -> PlatformCondition.CombinationResult { - // Skip already visited nodes - guard !visited.contains(root) else { return .incompatible } - visited.insert(root) + // if we're at a leaf dependency, there is nothing else to traverse. + guard let dependencies = graph.dependencies[rootDependency] else { return .incompatible } + + let result: PlatformCondition.CombinationResult + + // We've reached our destination, return a condition for the leaf relationship (`nil` or a `PlatformFilters` set) + if dependencies.contains(transitiveDependency) { + result = .condition(graph.dependencyConditions[(rootDependency, transitiveDependency)]) + } else { + // Capture the filters that could be applied to intermediate dependencies + // A --> (.ios) B --> C : C should have the .ios filter applied due to B + let filters = dependencies.map { node -> PlatformCondition.CombinationResult in + let transitive = combinedCondition(to: transitiveDependency, from: node) + let currentCondition = graph.dependencyConditions[(rootDependency, node)] + switch transitive { + case .incompatible: + return .incompatible + case let .condition(.some(condition)): + return condition.intersection(currentCondition) + case .condition: + return .condition(currentCondition) + } + } - // if we're at a leaf dependency, there is nothing else to traverse. - guard let dependencies = graph.dependencies[root] else { return .incompatible } + // Union our filters because multiple paths could lead to the same dependency (e.g. AVFoundation) + // A --> (.ios) B --> C + // A --> (.macos) D --> C + // C should have `[.ios, .macos]` set for filters to satisfy both paths + let transitiveFilters = filters.compactMap { $0 } + .reduce(PlatformCondition.CombinationResult.incompatible) { result, condition in + result.combineWith(condition) + } - // We've reached our destination, return the filters or `.all` if none are set - if dependencies.contains(other) { - return .condition(graph.dependencyConditions[(root, other)]) - } else { - // Capture the filters that could be applied to intermediate dependencies - // A --> (.ios) B --> C : C should have the .ios filter applied due to B - let filters = dependencies.map { node -> PlatformCondition.CombinationResult in - let transitive = find(from: node, to: other) - let currentCondition = graph.dependencyConditions[(root, node)] - switch transitive { + result = transitiveFilters + } + + conditionCache[(rootDependency, transitiveDependency)] = result + return result + } + + public func externalTargetSupportedPlatforms() -> [GraphTarget: Set] { + let targetsWithExternalDependencies = targetsWithExternalDependencies() + var platforms: [GraphTarget: Set] = [:] + + func traverse(target: GraphTarget, parentPlatforms: Set) { + let dependencies = directTargetDependencies(path: target.path, name: target.target.name) + + for dependencyTargetReference in dependencies { + var platformsToInsert: Set? + + let dependencyTarget = dependencyTargetReference.graphTarget + if let dependencyCondition = dependencyTargetReference.condition, + let platformIntersection = PlatformCondition.when(target.target.dependencyPlatformFilters)? + .intersection(dependencyCondition) + { + switch platformIntersection { case .incompatible: - return .incompatible - case let .condition(.some(condition)): - return condition.intersection(currentCondition) - case .condition: - return .condition(currentCondition) + break + case let .condition(condition): + if let condition { + let dependencyPlatforms: [Platform] = condition.platformFilters.map(\.platform).filter { $0 != nil } + .map { $0! } + platformsToInsert = Set(dependencyPlatforms) + } } + } else { + let inheritedPlatforms = dependencyTarget.target.product == .macro ? Set([.macOS]) : parentPlatforms + platformsToInsert = inheritedPlatforms.intersection(dependencyTarget.target.supportedPlatforms) } - // Union our filters because multiple paths could lead to the same dependency (e.g. AVFoundation) - // A --> (.ios) B --> C - // A --> (.macos) D --> C - // C should have `[.ios, .macos]` set for filters to satisfy both paths - let transitiveFilters = filters.compactMap { $0 } - .reduce(PlatformCondition.CombinationResult.incompatible) { result, condition in - result.combineWith(condition) - } + if let platformsToInsert { + var existingPlatforms = platforms[dependencyTarget, default: Set()] + let continueTraversing = !platformsToInsert.isSubset(of: existingPlatforms) + existingPlatforms.formUnion(platformsToInsert) + platforms[dependencyTarget] = existingPlatforms - return transitiveFilters + if continueTraversing { + traverse(target: dependencyTarget, parentPlatforms: platforms[dependencyTarget, default: Set()]) + } + } } } - return find(from: rootDependency, to: transitiveDependency) + targetsWithExternalDependencies.forEach { traverse(target: $0, parentPlatforms: $0.target.supportedPlatforms) } + return platforms } func allDependenciesSatisfy(from rootDependency: GraphDependency, meets: (GraphDependency) -> Bool) -> Bool { @@ -707,12 +880,22 @@ public class GraphTraverser: GraphTraversing { filterDependencies( from: dependency, test: isDependencyStatic, - skip: canDependencyLinkStaticProducts + skip: or(canDependencyLinkStaticProducts, isDependencyPrecompiledMacro) ) } + func isDependencyPrecompiledMacro(_ dependency: GraphDependency) -> Bool { + switch dependency { + case .macro: + return true + case .bundle, .framework, .xcframework, .library, .sdk, .target, .packageProduct: + return false + } + } + func isDependencyPrecompiledLibrary(dependency: GraphDependency) -> Bool { switch dependency { + case .macro: return false case .xcframework: return true case .framework: return true case .library: return true @@ -725,6 +908,7 @@ public class GraphTraverser: GraphTraversing { func isDependencyPrecompiledFramework(dependency: GraphDependency) -> Bool { switch dependency { + case .macro: return false case .xcframework: return true case .framework: return true case .library: return false @@ -736,16 +920,16 @@ public class GraphTraverser: GraphTraversing { } func isXCFrameworkMerged(dependency: GraphDependency, expectedMergedBinaries: Set) -> Bool { - guard case let .xcframework(_, infoPlist, _, _, mergeable, _) = dependency, - let binaryName = infoPlist.libraries.first?.binaryName, + guard case let .xcframework(xcframework) = dependency, + let binaryName = xcframework.infoPlist.libraries.first?.binaryName, expectedMergedBinaries.contains(binaryName) else { return false } - if !mergeable { + if !xcframework.mergeable { fatalError("XCFramework \(binaryName) must be compiled with -make_mergeable option enabled") } - return mergeable + return xcframework.mergeable } func isDependencyDynamicNonMergeableTarget(dependency: GraphDependency) -> Bool { @@ -762,8 +946,11 @@ public class GraphTraverser: GraphTraversing { func isDependencyStatic(dependency: GraphDependency) -> Bool { switch dependency { - case let .xcframework(_, _, _, linking, _, _), - let .framework(_, _, _, _, linking, _, _, _), + case .macro: + return false + case let .xcframework(xcframework): + return xcframework.linking == .static + case let .framework(_, _, _, _, linking, _, _, _), let .library(_, _, linking, _, _): return linking == .static case .bundle: return false @@ -789,6 +976,7 @@ public class GraphTraverser: GraphTraversing { func isDependencyDynamicTarget(dependency: GraphDependency) -> Bool { switch dependency { + case .macro: return false case .xcframework: return false case .framework: return false case .library: return false @@ -801,19 +989,6 @@ public class GraphTraverser: GraphTraversing { } } - func isDependencyPrecompiledDynamicAndLinkable(dependency: GraphDependency) -> Bool { - switch dependency { - case let .xcframework(_, _, _, linking, _, _), - let .framework(_, _, _, _, linking, _, _, _), - let .library(path: _, publicHeaders: _, linking: linking, architectures: _, swiftModuleMap: _): - return linking == .dynamic - case .bundle: return false - case .packageProduct: return false - case .target: return false - case .sdk: return false - } - } - func canDependencyEmbedProducts(dependency: GraphDependency) -> Bool { guard case let GraphDependency.target(name, path) = dependency, let target = target(path: path, name: name) else { return false } @@ -828,7 +1003,7 @@ public class GraphTraverser: GraphTraversing { func unitTestHost(path: AbsolutePath, name: String) -> GraphTarget? { directLocalTargetDependencies(path: path, name: name) - .first(where: { $0.target.product.canHostTests() }) + .first(where: { $0.target.product.canHostTests() })?.graphTarget } func canEmbedProducts(target: Target) -> Bool { @@ -844,6 +1019,7 @@ public class GraphTraverser: GraphTraversing { return validProducts.contains(target.product) } + // swiftlint:disable:next function_body_length func dependencyReference( to toDependency: GraphDependency, from fromDependency: GraphDependency @@ -853,6 +1029,8 @@ public class GraphTraverser: GraphTraversing { } switch toDependency { + case let .macro(path): + return .macro(path: path) case let .framework(path, binaryPath, dsymPath, bcsymbolmapPaths, linking, architectures, isCarthage, status): return .framework( path: path, @@ -892,13 +1070,13 @@ public class GraphTraverser: GraphTraversing { productName: target.target.productNameWithExtension, condition: condition ) - case let .xcframework(path, infoPlist, primaryBinaryPath, _, _, status): + case let .xcframework(xcframework): return .xcframework( - path: path, - infoPlist: infoPlist, - primaryBinaryPath: primaryBinaryPath, - binaryPath: primaryBinaryPath, - status: status, + path: xcframework.path, + infoPlist: xcframework.infoPlist, + primaryBinaryPath: xcframework.primaryBinaryPath, + binaryPath: xcframework.primaryBinaryPath, + status: xcframework.status, condition: condition ) } @@ -935,7 +1113,7 @@ public class GraphTraverser: GraphTraversing { switch dependency { case let .framework(_, _, _, _, linking: linking, _, _, _): return linking == .static - case .xcframework, .library, .bundle, .packageProduct, .target, .sdk: + case .xcframework, .library, .bundle, .packageProduct, .target, .sdk, .macro: return false } } @@ -955,13 +1133,13 @@ public class GraphTraverser: GraphTraversing { from: .target(name: name, path: path), test: { dependency in switch dependency { - case let .xcframework(_, _, _, linking: linking, _, _): - return linking == .static - case .framework, .library, .bundle, .packageProduct, .target, .sdk: + case let .xcframework(xcframework): + return xcframework.linking == .static + case .framework, .library, .bundle, .packageProduct, .target, .sdk, .macro: return false } }, - skip: { $0.isDynamicPrecompiled || !$0.isPrecompiled } + skip: { $0.isDynamicPrecompiled || !$0.isPrecompiled || $0.isPrecompiledMacro } ) return Set(dependencies) .compactMap { dependencyReference(to: $0, from: .target(name: name, path: path)) } diff --git a/Sources/TuistCore/Graph/ModelExtensions/Project+Core.swift b/Sources/TuistCore/Graph/ModelExtensions/Project+Core.swift index 3e15b85ba07..ac6226ff987 100644 --- a/Sources/TuistCore/Graph/ModelExtensions/Project+Core.swift +++ b/Sources/TuistCore/Graph/ModelExtensions/Project+Core.swift @@ -21,10 +21,10 @@ extension Project { // Second criteria: Most dependent targets first. let secondDependencies = graphTraverser.directTargetDependencies(path: self.path, name: second.name) - .filter { $0.path == self.path } + .filter { $0.graphTarget.path == self.path } .map(\.target.name) let firstDependencies = graphTraverser.directTargetDependencies(path: self.path, name: first.name) - .filter { $0.path == self.path } + .filter { $0.graphTarget.path == self.path } .map(\.target.name) if secondDependencies.contains(first.name) { diff --git a/Sources/TuistCore/Graph/ModelExtensions/Target+Core.swift b/Sources/TuistCore/Graph/ModelExtensions/Target+Core.swift index adb0d4d346e..4f36d8d1027 100644 --- a/Sources/TuistCore/Graph/ModelExtensions/Target+Core.swift +++ b/Sources/TuistCore/Graph/ModelExtensions/Target+Core.swift @@ -56,13 +56,13 @@ extension Target { var sourceFiles: [AbsolutePath: TuistGraph.SourceFile] = [:] var invalidGlobs: [InvalidGlob] = [] - try sources.forEach { source in + for source in sources { let sourcePath = try AbsolutePath(validating: source.glob) let base = try AbsolutePath(validating: sourcePath.dirname) // Paths that should be excluded from sources var excluded: [AbsolutePath] = [] - try source.excluding.forEach { path in + for path in source.excluding { let absolute = try AbsolutePath(validating: path) let globs = try AbsolutePath(validating: absolute.dirname).glob(absolute.basename) excluded.append(contentsOf: globs) diff --git a/Sources/TuistCore/GraphTraverser/GraphTraversing.swift b/Sources/TuistCore/GraphTraverser/GraphTraversing.swift index 492886bb66b..c11ae1f0279 100644 --- a/Sources/TuistCore/GraphTraverser/GraphTraversing.swift +++ b/Sources/TuistCore/GraphTraverser/GraphTraversing.swift @@ -71,35 +71,29 @@ public protocol GraphTraversing { /// - Returns: The test plans with the given name. func testPlan(name: String) -> TestPlan? + /// - Returns: All direct and transitive target dependencies + func allTargetDependencies(path: AbsolutePath, name: String) -> Set + /// Given a project directory and target name, it returns **all**l its direct target dependencies present in the same project. /// If you want only direct target dependencies present in the same project as the target, use `directLocalTargetDependencies` /// instead /// - Parameters: /// - path: Path to the directory that contains the target's project. /// - name: Target name. - func directTargetDependencies(path: AbsolutePath, name: String) -> Set + func directTargetDependencies(path: AbsolutePath, name: String) -> Set /// Given a project directory and target name, it returns all its direct target dependencies present in the same project. /// To get **all** direct target dependencies use the method `directTargetDependencies` instead /// - Parameters: /// - path: Path to the directory that contains the project. /// - name: Target name. - func directLocalTargetDependencies(path: AbsolutePath, name: String) -> Set - - /// Given a project directory and a target name, it returns all direct dependencies with their conditions - /// - Parameters: - /// - path: Path to the directory that contains the project. - /// - name: Target name. - func directLocalTargetDependenciesWithConditions(path: AbsolutePath, name: String) -> [( - GraphTarget, - PlatformCondition? - )] + func directLocalTargetDependencies(path: AbsolutePath, name: String) -> Set /// Given a project directory and a target name, it returns all the dependencies that are extensions. /// - Parameters: /// - path: Path to the directory that contains the project. /// - name: Target name. - func appExtensionDependencies(path: AbsolutePath, name: String) -> Set + func appExtensionDependencies(path: AbsolutePath, name: String) -> Set /// Returns the transitive resource bundle dependencies for the given target. /// - Parameters: @@ -126,7 +120,7 @@ public protocol GraphTraversing { /// - Parameters: /// - path: Path to the directory that contains the project. /// - name: Target name. - func appClipDependencies(path: AbsolutePath, name: String) -> GraphTarget? + func appClipDependencies(path: AbsolutePath, name: String) -> GraphTargetReference? /// Given a project directory and a target name, it returns the list of dependencies that need to be embedded into the target /// product. @@ -204,7 +198,7 @@ public protocol GraphTraversing { /// - Parameters: /// - path: Path to the directory that contains the project. /// - name: Target name. - func extensionKitExtensionDependencies(path: AbsolutePath, name: String) -> Set + func extensionKitExtensionDependencies(path: AbsolutePath, name: String) -> Set /// Given a project and a target name, it returns all the direct target dependencies of the target that represent Swift Macro /// executables. @@ -213,12 +207,59 @@ public protocol GraphTraversing { /// - name: Target name. func directSwiftMacroExecutables(path: AbsolutePath, name: String) -> Set - /// Given a project and a target name, it returns all the direct target dependencies that are a static framework representing + /// Given a project and a target name, it returns all the direct target dependencies that are a target representing + /// a Swift Macro + /// - Parameters: + /// - path: Path to the directory that contains the project. + /// - name: Target name. + func directSwiftMacroTargets(path: AbsolutePath, name: String) -> Set + + /// Given a project and a target name, it returns all the target dependencies that are a target representing /// a Swift Macro /// - Parameters: /// - path: Path to the directory that contains the project. /// - name: Target name. - func directSwiftMacroFrameworkTargets(path: AbsolutePath, name: String) -> Set + func allSwiftMacroTargets(path: AbsolutePath, name: String) -> Set + + /// It returns a set containing the external dependencies that are not referenced by the projects either directly nor + /// transitively. + /// - Returns: The list of dependencies. + func allOrphanExternalTargets() -> Set + + /// Returns all the non-external targets of the graph that depend on a external target. + /// - Returns: A set containing all the targets. + func targetsWithExternalDependencies() -> Set + + /// Returns all the targets that are part of external projects. + /// - Returns: A set containing all the external project targets + func allExternalTargets() -> Set + + /// External targets (e.g. from packages) might indicate that they support platforms that + /// they don't really support. To prevent this from causing compilation issues, Tuist cascades + /// the supported platforms down to the external dependencies. + /// This function narrows down the platforms of the external dependencies and returns a + /// dictionary containing the graph target as a key, and the supported platforms as the value. + /// - Returns: A dictionary with the graph targets as keys, and the platforms that they support + /// as values + func externalTargetSupportedPlatforms() -> [GraphTarget: Set] + + /// Given a target's project path and name, it returns its target dependencies that are external. + /// - Parameters: + /// - path: Project path. + /// - name: Target name. + /// - Returns: A set containing all the direct target dependencies that are external. + func directTargetExternalDependencies(path: AbsolutePath, name: String) -> Set + + /// It returns all the plugin executables to use for a given target. A plugin executable can represent a Swift Macro + /// executable to resolve macros. + /// + /// Executables are passed through the build setting "OTHER_SWIFT_FLAGS" and the flag "-load-plugin-executable {value}" + /// + /// + /// - Parameters: + /// - path: Path to the directory that contains the project. + /// - name: Target name. + func allSwiftPluginExecutables(path: AbsolutePath, name: String) -> Set } extension GraphTraversing { diff --git a/Sources/TuistCore/Log/Logger.swift b/Sources/TuistCore/Log/Logger.swift index 47524ec4e13..f00ac15166a 100644 --- a/Sources/TuistCore/Log/Logger.swift +++ b/Sources/TuistCore/Log/Logger.swift @@ -1,2 +1,3 @@ import TuistSupport + let logger = Logger(label: "io.tuist.core") diff --git a/Sources/TuistCore/MetadataProviders/XCFrameworkMetadataProvider.swift b/Sources/TuistCore/MetadataProviders/XCFrameworkMetadataProvider.swift index e3e4f7ce0f1..7543583c620 100644 --- a/Sources/TuistCore/MetadataProviders/XCFrameworkMetadataProvider.swift +++ b/Sources/TuistCore/MetadataProviders/XCFrameworkMetadataProvider.swift @@ -54,12 +54,14 @@ public protocol XCFrameworkMetadataProviding: PrecompiledMetadataProviding { // MARK: - Default Implementation public final class XCFrameworkMetadataProvider: PrecompiledMetadataProvider, XCFrameworkMetadataProviding { - override public init() { + let fileHandler: FileHandling + + public init(fileHandler: FileHandling = FileHandler.shared) { + self.fileHandler = fileHandler super.init() } public func loadMetadata(at path: AbsolutePath, status: FrameworkStatus) throws -> XCFrameworkMetadata { - let fileHandler = FileHandler.shared guard fileHandler.exists(path) else { throw XCFrameworkMetadataProviderError.xcframeworkNotFound(path) } @@ -75,12 +77,23 @@ public final class XCFrameworkMetadataProvider: PrecompiledMetadataProvider, XCF primaryBinaryPath: primaryBinaryPath, linking: linking, mergeable: infoPlist.libraries.allSatisfy(\.mergeable), - status: status + status: status, + macroPath: try macroPath(xcframeworkPath: path) ) } + /** + An XCFramework that contains static frameworks that represent macros, those are frameworks with a Macros directory in them. + We assume that the Swift Macros, which are command line executables, are fat binaries for both architectures supported by macOS: + x86_64 and arm64. + */ + public func macroPath(xcframeworkPath: AbsolutePath) throws -> AbsolutePath? { + guard let frameworkPath = fileHandler.glob(xcframeworkPath, glob: "*/*.framework").sorted().first else { return nil } + guard let macroPath = fileHandler.glob(frameworkPath, glob: "Macros/*").first else { return nil } + return try AbsolutePath(validating: macroPath.pathString) + } + public func infoPlist(xcframeworkPath: AbsolutePath) throws -> XCFrameworkInfoPlist { - let fileHandler = FileHandler.shared let infoPlist = xcframeworkPath.appending(component: "Info.plist") guard fileHandler.exists(infoPlist) else { throw XCFrameworkMetadataProviderError.missingRequiredFile(infoPlist) @@ -100,7 +113,7 @@ public final class XCFrameworkMetadataProvider: PrecompiledMetadataProvider, XCF ) else { return false } - guard FileHandler.shared.exists(binaryPath) else { + guard fileHandler.exists(binaryPath) else { // The missing slice relative to the XCFramework folder. e.g ios-x86_64-simulator/Alamofire.framework/Alamofire let relativeArchitectureBinaryPath = binaryPath.components.suffix(3).joined(separator: "/") logger diff --git a/Sources/TuistCore/NodeLoaders/XCFrameworkLoader.swift b/Sources/TuistCore/NodeLoaders/XCFrameworkLoader.swift index bcf76219f63..8b293b4772b 100644 --- a/Sources/TuistCore/NodeLoaders/XCFrameworkLoader.swift +++ b/Sources/TuistCore/NodeLoaders/XCFrameworkLoader.swift @@ -52,13 +52,15 @@ public final class XCFrameworkLoader: XCFrameworkLoading { at: path, status: status ) - return .xcframework( + let xcframework = GraphDependency.XCFramework( path: path, infoPlist: metadata.infoPlist, primaryBinaryPath: metadata.primaryBinaryPath, linking: metadata.linking, mergeable: metadata.mergeable, - status: metadata.status + status: metadata.status, + macroPath: metadata.macroPath ) + return .xcframework(xcframework) } } diff --git a/Sources/TuistCoreTesting/Automation/MockXcodeBuildController.swift b/Sources/TuistCoreTesting/Automation/MockXcodeBuildController.swift index fbf5e540d47..f4007b01c6b 100644 --- a/Sources/TuistCoreTesting/Automation/MockXcodeBuildController.swift +++ b/Sources/TuistCoreTesting/Automation/MockXcodeBuildController.swift @@ -12,8 +12,7 @@ final class MockXcodeBuildController: XcodeBuildControlling { Bool, AbsolutePath?, Bool, - [XcodeBuildArgument], - Bool + [XcodeBuildArgument] ) -> [SystemEvent])? func build( @@ -23,8 +22,7 @@ final class MockXcodeBuildController: XcodeBuildControlling { rosetta: Bool, derivedDataPath: AbsolutePath?, clean: Bool, - arguments: [XcodeBuildArgument], - rawXcodebuildLogs: Bool + arguments: [XcodeBuildArgument] ) -> AsyncThrowingStream, Error> { if let buildStub { return buildStub( @@ -34,8 +32,7 @@ final class MockXcodeBuildController: XcodeBuildControlling { rosetta, derivedDataPath, clean, - arguments, - rawXcodebuildLogs + arguments ).asAsyncThrowingStream() } else { return AsyncThrowingStream { @@ -59,8 +56,7 @@ final class MockXcodeBuildController: XcodeBuildControlling { Int, [TestIdentifier], [TestIdentifier], - TestPlanConfiguration?, - Bool + TestPlanConfiguration? ) -> [SystemEvent] )? @@ -77,8 +73,7 @@ final class MockXcodeBuildController: XcodeBuildControlling { retryCount: Int, testTargets: [TestIdentifier], skipTestTargets: [TestIdentifier], - testPlanConfiguration: TestPlanConfiguration?, - rawXcodebuildLogs: Bool + testPlanConfiguration: TestPlanConfiguration? ) -> AsyncThrowingStream, Error> { if let testStub { let results = testStub( @@ -93,8 +88,7 @@ final class MockXcodeBuildController: XcodeBuildControlling { retryCount, testTargets, skipTestTargets, - testPlanConfiguration, - rawXcodebuildLogs + testPlanConfiguration ) if let testErrorStub { return AsyncThrowingStream { @@ -113,7 +107,7 @@ final class MockXcodeBuildController: XcodeBuildControlling { } var archiveStub: ( - (XcodeBuildTarget, String, Bool, AbsolutePath, [XcodeBuildArgument], Bool) + (XcodeBuildTarget, String, Bool, AbsolutePath, [XcodeBuildArgument], AbsolutePath?) -> [SystemEvent] )? func archive( @@ -122,10 +116,11 @@ final class MockXcodeBuildController: XcodeBuildControlling { clean: Bool, archivePath: AbsolutePath, arguments: [XcodeBuildArgument], - rawXcodebuildLogs: Bool + derivedDataPath: AbsolutePath? ) -> AsyncThrowingStream, Error> { if let archiveStub { - return archiveStub(target, scheme, clean, archivePath, arguments, rawXcodebuildLogs).asAsyncThrowingStream() + return archiveStub(target, scheme, clean, archivePath, arguments, derivedDataPath) + .asAsyncThrowingStream() } else { return AsyncThrowingStream { throw TestError( @@ -135,14 +130,16 @@ final class MockXcodeBuildController: XcodeBuildControlling { } } - var createXCFrameworkStub: (([AbsolutePath], AbsolutePath, Bool) -> [SystemEvent])? + var createXCFrameworkStub: ( + ([XcodeBuildControllerCreateXCFrameworkArgument], AbsolutePath) + -> [SystemEvent] + )? func createXCFramework( - frameworks: [AbsolutePath], - output: AbsolutePath, - rawXcodebuildLogs: Bool + arguments: [XcodeBuildControllerCreateXCFrameworkArgument], + output: AbsolutePath ) -> AsyncThrowingStream, Error> { if let createXCFrameworkStub { - return createXCFrameworkStub(frameworks, output, rawXcodebuildLogs).asAsyncThrowingStream() + return createXCFrameworkStub(arguments, output).asAsyncThrowingStream() } else { return AsyncThrowingStream { throw TestError( @@ -152,14 +149,15 @@ final class MockXcodeBuildController: XcodeBuildControlling { } } - var showBuildSettingsStub: ((XcodeBuildTarget, String, String) -> [String: XcodeBuildSettings])? + var showBuildSettingsStub: ((XcodeBuildTarget, String, String, AbsolutePath?) -> [String: XcodeBuildSettings])? func showBuildSettings( _ target: XcodeBuildTarget, scheme: String, - configuration: String + configuration: String, + derivedDataPath: AbsolutePath? ) throws -> [String: XcodeBuildSettings] { if let showBuildSettingsStub { - return showBuildSettingsStub(target, scheme, configuration) + return showBuildSettingsStub(target, scheme, configuration, derivedDataPath) } else { throw TestError( "\(String(describing: MockXcodeBuildController.self)) received an unexpected call to showBuildSettings" diff --git a/Sources/TuistCoreTesting/Graph/GraphDependencyReference+TestData.swift b/Sources/TuistCoreTesting/Graph/GraphDependencyReference+TestData.swift index 84a68090bec..4a3d73d3005 100644 --- a/Sources/TuistCoreTesting/Graph/GraphDependencyReference+TestData.swift +++ b/Sources/TuistCoreTesting/Graph/GraphDependencyReference+TestData.swift @@ -32,6 +32,12 @@ extension GraphDependencyReference { ) } + public static func testMacro( + path: AbsolutePath = "/macros/tuist" + ) -> GraphDependencyReference { + GraphDependencyReference.macro(path: path) + } + public static func testXCFramework( path: AbsolutePath = "/frameworks/tuist.xcframework", infoPlist: XCFrameworkInfoPlist = .test(), diff --git a/Sources/TuistCoreTesting/GraphTraverser/MockGraphTraverser.swift b/Sources/TuistCoreTesting/GraphTraverser/MockGraphTraverser.swift index 7954f50ab76..1572f2b29fa 100644 --- a/Sources/TuistCoreTesting/GraphTraverser/MockGraphTraverser.swift +++ b/Sources/TuistCoreTesting/GraphTraverser/MockGraphTraverser.swift @@ -235,6 +235,20 @@ final class MockGraphTraverser: GraphTraversing { return stubbedTargetsAtResult } + var invokedAllTargetDependencies = false + var invokedAllTargetDependenciesCount = 0 + var invokedAllTargetDependenciesParameters: ( + path: AbsolutePath, + name: String + )? + var invokedAllTargetDependenciesResult: Set = [] + func allTargetDependencies(path: TSCBasic.AbsolutePath, name: String) -> Set { + invokedAllTargetDependencies = true + invokedAllTargetDependenciesCount += 1 + invokedAllTargetDependenciesParameters = (path, name) + return invokedAllTargetDependenciesResult + } + var invokedDirectLocalTargetDependencies = false var invokedDirectLocalTargetDependenciesCount = 0 @@ -244,9 +258,9 @@ final class MockGraphTraverser: GraphTraversing { )? var invokedDirectLocalTargetDependenciesParametersList = [(path: AbsolutePath, name: String)]() - var stubbedDirectLocalTargetDependenciesResult: Set! = [] + var stubbedDirectLocalTargetDependenciesResult: Set! = [] - func directLocalTargetDependencies(path: AbsolutePath, name: String) -> Set { + func directLocalTargetDependencies(path: AbsolutePath, name: String) -> Set { invokedDirectLocalTargetDependencies = true invokedDirectLocalTargetDependenciesCount += 1 invokedDirectLocalTargetDependenciesParameters = (path, name) @@ -261,29 +275,15 @@ final class MockGraphTraverser: GraphTraversing { path: AbsolutePath, name: String )? - var invokedDirectLocalTargetDependenciesWithConditionsParametersList = - [(path: AbsolutePath, name: String)]() - var stubbedDirectLocalTargetDependenciesWithConditionsResult: [(GraphTarget, PlatformCondition?)]! = [] - - func directLocalTargetDependenciesWithConditions(path: TSCBasic.AbsolutePath, name: String) -> [( - TuistGraph.GraphTarget, - TuistGraph.PlatformCondition? - )] { - invokedDirectLocalTargetDependenciesWithConditions = true - invokedDirectLocalTargetDependenciesWithConditionsCount += 1 - invokedDirectLocalTargetDependenciesWithConditionsParameters = (path, name) - invokedDirectLocalTargetDependenciesWithConditionsParametersList.append((path, name)) - return stubbedDirectLocalTargetDependenciesWithConditionsResult - } var invokedDirectTargetDependencies = false var invokedDirectTargetDependenciesCount = 0 var invokedDirectTargetDependenciesParameters: (path: AbsolutePath, name: String)? var invokedDirectTargetDependenciesParametersList = [(path: AbsolutePath, name: String)]() - var stubbedDirectTargetDependenciesResult: Set! = [] + var stubbedDirectTargetDependenciesResult: Set! = [] - func directTargetDependencies(path: AbsolutePath, name: String) -> Set { + func directTargetDependencies(path: AbsolutePath, name: String) -> Set { invokedDirectTargetDependencies = true invokedDirectTargetDependenciesCount += 1 invokedDirectTargetDependenciesParameters = (path, name) @@ -296,9 +296,9 @@ final class MockGraphTraverser: GraphTraversing { var invokedAppExtensionDependenciesParameters: (path: AbsolutePath, name: String)? var invokedAppExtensionDependenciesParametersList = [(path: AbsolutePath, name: String)]() - var stubbedAppExtensionDependenciesResult: Set! = [] + var stubbedAppExtensionDependenciesResult: Set! = [] - func appExtensionDependencies(path: AbsolutePath, name: String) -> Set { + func appExtensionDependencies(path: AbsolutePath, name: String) -> Set { invokedAppExtensionDependencies = true invokedAppExtensionDependenciesCount += 1 invokedAppExtensionDependenciesParameters = (path, name) @@ -340,9 +340,9 @@ final class MockGraphTraverser: GraphTraversing { var invokedAppClipDependenciesCount = 0 var invokedAppClipDependenciesParameters: (path: AbsolutePath, name: String)? var invokedAppClipDependenciesParametersList = [(path: AbsolutePath, name: String)]() - var stubbedAppClipDependenciesResult: GraphTarget! + var stubbedAppClipDependenciesResult: GraphTargetReference! - func appClipDependencies(path: AbsolutePath, name: String) -> GraphTarget? { + func appClipDependencies(path: AbsolutePath, name: String) -> GraphTargetReference? { invokedAppClipDependencies = true invokedAppClipDependenciesCount += 1 invokedAppClipDependenciesParameters = (path, name) @@ -350,6 +350,20 @@ final class MockGraphTraverser: GraphTraversing { return stubbedAppClipDependenciesResult } + var invokedAppClipDependenciesWithConditions = false + var invokedAppClipDependenciesWithConditionsCount = 0 + var invokedAppClipDependenciesWithConditionsParameters: (path: AbsolutePath, name: String)? + var invokedAppClipDependenciesWithConditionsParametersList = [(path: AbsolutePath, name: String)]() + var stubbedAppClipDependenciesWithConditionsResult: (GraphTarget, PlatformCondition?)! + + func appClipDependenciesWithConditions(path: AbsolutePath, name: String) -> (GraphTarget, PlatformCondition?)? { + invokedAppClipDependenciesWithConditions = true + invokedAppClipDependenciesWithConditionsCount += 1 + invokedAppClipDependenciesWithConditionsParameters = (path, name) + invokedAppClipDependenciesWithConditionsParametersList.append((path, name)) + return stubbedAppClipDependenciesWithConditionsResult + } + var invokedEmbeddableFrameworks = false var invokedEmbeddableFrameworksCount = 0 var invokedEmbeddableFrameworksParameters: (path: AbsolutePath, name: String)? @@ -536,9 +550,9 @@ final class MockGraphTraverser: GraphTraversing { var invokedExtensionKitExtensionDependenciesParameters: (path: AbsolutePath, name: String)? var invokedExtensionKitExtensionDependenciesParametersList = [(path: AbsolutePath, name: String)]() - var stubbedExtensionKitExtensionDependenciesResult: Set! = [] + var stubbedExtensionKitExtensionDependenciesResult: Set! = [] - func extensionKitExtensionDependencies(path: TSCBasic.AbsolutePath, name: String) -> Set { + func extensionKitExtensionDependencies(path: TSCBasic.AbsolutePath, name: String) -> Set { invokedExtensionKitExtensionDependencies = true invokedExtensionKitExtensionDependenciesCount += 1 invokedExtensionKitExtensionDependenciesParameters = (path, name) @@ -546,6 +560,26 @@ final class MockGraphTraverser: GraphTraversing { return stubbedExtensionKitExtensionDependenciesResult } + var invokedExtensionKitExtensionDependenciesWithConditions = false + var invokedExtensionKitExtensionDependenciesWithConditionsCount = 0 + // swiftlint:disable:next identifier_name + var invokedExtensionKitExtensionDependenciesWithConditionsParameters: (path: AbsolutePath, name: String)? + // swiftlint:disable:next identifier_name + var invokedExtensionKitExtensionDependenciesWithConditionsParametersList = + [(path: AbsolutePath, name: String)]() + var stubbedExtensionKitExtensionDependenciesWithConditionsResult: [(GraphTarget, PlatformCondition?)]! = [] + + func extensionKitExtensionDependenciesWithConditions(path: TSCBasic.AbsolutePath, name: String) -> [( + TuistGraph.GraphTarget, + TuistGraph.PlatformCondition? + )] { + invokedExtensionKitExtensionDependenciesWithConditions = true + invokedExtensionKitExtensionDependenciesWithConditionsCount += 1 + invokedExtensionKitExtensionDependenciesWithConditionsParameters = (path, name) + invokedExtensionKitExtensionDependenciesWithConditionsParametersList.append((path, name)) + return stubbedExtensionKitExtensionDependenciesWithConditionsResult + } + var invokedDirectSwiftMacroExecutables = false var invokedDirectSwiftMacroExecutablesCount = 0 var invokedDirectSwiftMacroExecutablesParameters: (path: AbsolutePath, name: String)? @@ -561,18 +595,95 @@ final class MockGraphTraverser: GraphTraversing { return stubbedDirectSwiftMacroExecutablesResult } - var invokedDirectSwiftMacroFrameworkTargets = false - var invokedDirectSwiftMacroFrameworkTargetsCount = 0 - var invokedDirectSwiftMacroFrameworkTargetsParameters: (path: AbsolutePath, name: String)? - var invokedDirectSwiftMacroFrameworkTargetsParametersList = + var invokedDirectSwiftMacroTargets = false + var invokedDirectSwiftMacroTargetsCount = 0 + var invokedDirectSwiftMacroTargetsParameters: (path: AbsolutePath, name: String)? + var invokedDirectSwiftMacroTargetsParametersList = + [(path: AbsolutePath, name: String)]() + var stubbedDirectSwiftMacroTargetsResult: Set! = [] + func directSwiftMacroTargets(path: TSCBasic.AbsolutePath, name: String) -> Set { + invokedDirectSwiftMacroTargets = true + invokedDirectSwiftMacroTargetsCount += 1 + invokedDirectSwiftMacroTargetsParameters = (path, name) + invokedDirectSwiftMacroTargetsParametersList.append((path, name)) + return stubbedDirectSwiftMacroTargetsResult + } + + var invokedAllSwiftMacroTargets = false + var invokedAllSwiftMacroTargetsCount = 0 + var invokedAllSwiftMacroTargetsParameters: (path: AbsolutePath, name: String)? + var invokedAllSwiftMacroTargetsParametersList = + [(path: AbsolutePath, name: String)]() + var stubbedAllSwiftMacroTargetsResult: Set! = [] + func allSwiftMacroTargets(path: TSCBasic.AbsolutePath, name: String) -> Set { + invokedAllSwiftMacroTargets = true + invokedAllSwiftMacroTargetsCount += 1 + invokedAllSwiftMacroTargetsParameters = (path, name) + invokedAllSwiftMacroTargetsParametersList.append((path, name)) + return stubbedAllSwiftMacroTargetsResult + } + + var invokedAllOrphanExternalTargets = false + var invokedAllOrphanExternalTargetsCount = 0 + var stubbedAllOrphanExternalTargetsResult: Set! = [] + func allOrphanExternalTargets() -> Set { + invokedAllOrphanExternalTargets = true + invokedAllOrphanExternalTargetsCount += 1 + return stubbedAllOrphanExternalTargetsResult + } + + var invokedTargetsWithExternalDependencies = false + var invokedTargetsWithExternalDependenciesCount = 0 + var stubbedTargetsWithExternalDependenciesResult: Set! = [] + func targetsWithExternalDependencies() -> Set { + invokedTargetsWithExternalDependencies = true + invokedTargetsWithExternalDependenciesCount += 1 + return stubbedTargetsWithExternalDependenciesResult + } + + var invokedAllExternalTargets = false + var invokedAllExternalTargetsCount = 0 + var stubbedAllExternalTargetsResult: Set! = [] + func allExternalTargets() -> Set { + invokedAllExternalTargets = true + invokedAllExternalTargetsCount += 1 + return stubbedAllExternalTargetsResult + } + + var invokedExternalTargetSupportedPlatforms = false + var invokedExternalTargetSupportedPlatformsCount = 0 + var stubbedExternalTargetSupportedPlatformsResult: [GraphTarget: Set]! = [:] + func externalTargetSupportedPlatforms() -> [GraphTarget: Set] { + invokedExternalTargetSupportedPlatforms = true + invokedExternalTargetSupportedPlatformsCount += 1 + return stubbedExternalTargetSupportedPlatformsResult + } + + var invokedDirectTargetExternalDependencies = false + var invokedDirectTargetExternalDependenciesCount = 0 + var invokedDirectTargetExternalDependenciesParameters: (path: AbsolutePath, name: String)? + var invokedDirectTargetExternalDependenciesParametersList = + [(path: AbsolutePath, name: String)]() + var stubbedDirectTargetExternalDependenciesResult: Set! = [] + func directTargetExternalDependencies(path: AbsolutePath, name: String) -> Set { + invokedDirectTargetExternalDependencies = true + invokedDirectTargetExternalDependenciesCount += 1 + invokedDirectTargetExternalDependenciesParameters = (path, name) + invokedDirectTargetExternalDependenciesParametersList.append((path, name)) + return stubbedDirectTargetExternalDependenciesResult + } + + var invokedAllSwiftPluginExecutables = false + var invokedAllSwiftPluginExecutablesCount = 0 + var invokedAllSwiftPluginExecutablesParameters: (path: AbsolutePath, name: String)? + var invokedAllSwiftPluginExecutablesParametersList = [(path: AbsolutePath, name: String)]() - var stubbedDirectSwiftMacroFrameworkTargetsResult: Set! = [] - - func directSwiftMacroFrameworkTargets(path: TSCBasic.AbsolutePath, name: String) -> Set { - invokedDirectSwiftMacroFrameworkTargets = true - invokedDirectSwiftMacroFrameworkTargetsCount += 1 - invokedDirectSwiftMacroFrameworkTargetsParameters = (path, name) - invokedDirectSwiftMacroFrameworkTargetsParametersList.append((path, name)) - return stubbedDirectSwiftMacroFrameworkTargetsResult + var stubbedAllSwiftPluginExecutablesResult: Set! = [] + func allSwiftPluginExecutables(path: TSCBasic.AbsolutePath, name: String) -> Set { + invokedAllSwiftPluginExecutables = true + invokedAllSwiftPluginExecutablesCount += 1 + invokedAllSwiftPluginExecutablesParameters = (path, name) + invokedAllSwiftPluginExecutablesParametersList.append((path, name)) + return stubbedAllSwiftPluginExecutablesResult } } diff --git a/Sources/TuistDependencies/DependenciesController.swift b/Sources/TuistDependencies/DependenciesController.swift index 6ef6ec96a10..db4b81777a2 100644 --- a/Sources/TuistDependencies/DependenciesController.swift +++ b/Sources/TuistDependencies/DependenciesController.swift @@ -65,6 +65,16 @@ public protocol DependenciesControlling { swiftVersion: TSCUtility.Version? ) throws -> TuistCore.DependenciesGraph + /// Fetches dependencies. + /// - Parameter path: Directory where project's dependencies will be fetched. + /// - Parameter packageSettings: Custom Swift Package Manager settings + /// - Parameter swiftVersion: The specified version of Swift. If `nil` is passed then the environment’s version will be used. + func fetch( + at path: AbsolutePath, + packageSettings: TuistGraph.PackageSettings, + swiftVersion: TSCUtility.Version? + ) throws -> TuistCore.DependenciesGraph + /// Updates dependencies. /// - Parameters: /// - path: Directory where project's dependencies will be updated. @@ -76,6 +86,17 @@ public protocol DependenciesControlling { swiftVersion: TSCUtility.Version? ) throws -> TuistCore.DependenciesGraph + /// Updates dependencies. + /// - Parameters: + /// - path: Directory where project's dependencies will be updated. + /// - packageSettings: Custom Swift Package Manager settings + /// - swiftVersion: The specified version of Swift. If `nil` is passed then will use the environment’s version will be used. + func update( + at path: AbsolutePath, + packageSettings: TuistGraph.PackageSettings, + swiftVersion: TSCUtility.Version? + ) throws -> TuistCore.DependenciesGraph + /// Save dependencies graph. /// - Parameters: /// - dependenciesGraph: The dependencies graph to be saved. @@ -116,6 +137,54 @@ public final class DependenciesController: DependenciesControlling { ) } + public func fetch( + at path: AbsolutePath, + packageSettings: TuistGraph.PackageSettings, + swiftVersion: TSCUtility.Version? + ) throws -> TuistCore.DependenciesGraph { + try install( + at: path, + dependencies: TuistGraph.Dependencies( + carthage: nil, + swiftPackageManager: TuistGraph.SwiftPackageManagerDependencies( + .manifest, + productTypes: packageSettings.productTypes, + baseSettings: packageSettings.baseSettings, + targetSettings: packageSettings.targetSettings, + projectOptions: packageSettings.projectOptions + + ), + platforms: packageSettings.platforms + ), + shouldUpdate: false, + swiftVersion: swiftVersion + ) + } + + public func update( + at path: AbsolutePath, + packageSettings: TuistGraph.PackageSettings, + swiftVersion: TSCUtility.Version? + ) throws -> TuistCore.DependenciesGraph { + try install( + at: path, + dependencies: TuistGraph.Dependencies( + carthage: nil, + swiftPackageManager: TuistGraph.SwiftPackageManagerDependencies( + .manifest, + productTypes: packageSettings.productTypes, + baseSettings: packageSettings.baseSettings, + targetSettings: packageSettings.targetSettings, + projectOptions: packageSettings.projectOptions + + ), + platforms: packageSettings.platforms + ), + shouldUpdate: true, + swiftVersion: swiftVersion + ) + } + public func update( at path: AbsolutePath, dependencies: TuistGraph.Dependencies, @@ -189,7 +258,7 @@ extension TuistCore.DependenciesGraph { var mergedExternalDependencies: [String: [ProjectDescription.TargetDependency]] = externalDependencies - try other.externalDependencies.forEach { name, dependency in + for (name, dependency) in other.externalDependencies { if let alreadyPresent = mergedExternalDependencies[name] { throw DependenciesControllerError.duplicatedDependency(name, alreadyPresent, dependency) } diff --git a/Sources/TuistDependencies/DependenciesGraph/DependenciesGraphController.swift b/Sources/TuistDependencies/DependenciesGraph/DependenciesGraphController.swift index 7dcf326d4b2..499e06ac2bd 100644 --- a/Sources/TuistDependencies/DependenciesGraph/DependenciesGraphController.swift +++ b/Sources/TuistDependencies/DependenciesGraph/DependenciesGraphController.swift @@ -88,7 +88,14 @@ public final class DependenciesGraphController: DependenciesGraphControlling { let dependenciesPath = dependenciesPath(at: rootDirectory) - guard FileHandler.shared.exists(dependenciesPath) else { + guard FileHandler.shared.exists(dependenciesPath) + || FileHandler.shared.exists( + rootDirectory.appending(components: [ + Constants.tuistDirectoryName, + Constants.DependenciesDirectory.packageSwiftName, + ]) + ) + else { return .none } diff --git a/Sources/TuistDependencies/Log/Logger.swift b/Sources/TuistDependencies/Log/Logger.swift index cbbcd1c3325..ec8780f3e1e 100644 --- a/Sources/TuistDependencies/Log/Logger.swift +++ b/Sources/TuistDependencies/Log/Logger.swift @@ -1,2 +1,3 @@ import TuistSupport + let logger = Logger(label: "io.tuist.dependencies") diff --git a/Sources/TuistDependencies/Mappers/ExternalProjectsPlatformNarrowerGraphMapper.swift b/Sources/TuistDependencies/Mappers/ExternalProjectsPlatformNarrowerGraphMapper.swift new file mode 100644 index 00000000000..f4388b9057c --- /dev/null +++ b/Sources/TuistDependencies/Mappers/ExternalProjectsPlatformNarrowerGraphMapper.swift @@ -0,0 +1,78 @@ +import Foundation +import TuistCore +import TuistGraph + +/** + When Swift Packages don't declare the platforms that they support, the Swift Package Manager defaults the value + to 'support all the platforms'. This default behaviour is inherited into the Xcode projects that we generate off the packages + and that causes compilation issues. Xcode must resolve this issue at build-time by cascading the platform requirements + down from nodes in the graph that are closer to the root. This is a behaviour that we need to copy over to Tuist. In our case + the logic is executed at generation time. + */ +public struct ExternalProjectsPlatformNarrowerGraphMapper: GraphMapping { // swiftlint:disable:this type_name + public init() {} + + public func map(graph: Graph) async throws -> (Graph, [TuistCore.SideEffectDescriptor]) { + // If the project has no external dependencies we skip this. + if graph.projects.values.first(where: { $0.isExternal }) == nil { + return (graph, []) + } + + var graph = graph + let externalTargetSupportedPlatforms = GraphTraverser(graph: graph).externalTargetSupportedPlatforms() + + graph.targets = Dictionary(uniqueKeysWithValues: graph.targets.map { projectPath, projectTargets in + let project = graph.projects[projectPath]! + let projectTargets = Dictionary(uniqueKeysWithValues: projectTargets.map { targetName, target in + ( + targetName, + mapTarget( + target: target, + project: project, + externalTargetSupportedPlatforms: externalTargetSupportedPlatforms + ) + ) + }) + return (projectPath, projectTargets) + }) + graph.projects = Dictionary(uniqueKeysWithValues: graph.projects.map { projectPath, project in + var project = project + project.targets = project.targets.map { mapTarget( + target: $0, + project: project, + externalTargetSupportedPlatforms: externalTargetSupportedPlatforms + ) } + return (projectPath, project) + }) + + return (graph, []) + } + + private func mapTarget( + target: Target, + project: Project, + externalTargetSupportedPlatforms: [GraphTarget: Set] + ) -> Target { + /** + We only include the destinations whose platform is included in the list of the target supported platforms. + */ + var target = target + let graphTarget = GraphTarget(path: project.path, target: target, project: project) + if project.isExternal, let targetFilteredPlatforms = externalTargetSupportedPlatforms[graphTarget] { + target.destinations = target.destinations.filter { destination in + targetFilteredPlatforms.contains(destination.platform) + } + + // By changing the destinations we also need to adapt the deployment targets accordingly to account for possibly + // removed destinations + target.deploymentTargets = .init( + iOS: targetFilteredPlatforms.contains(.iOS) ? target.deploymentTargets.iOS : nil, + macOS: targetFilteredPlatforms.contains(.macOS) ? target.deploymentTargets.macOS : nil, + watchOS: targetFilteredPlatforms.contains(.watchOS) ? target.deploymentTargets.watchOS : nil, + tvOS: targetFilteredPlatforms.contains(.tvOS) ? target.deploymentTargets.tvOS : nil, + visionOS: targetFilteredPlatforms.contains(.visionOS) ? target.deploymentTargets.visionOS : nil + ) + } + return target + } +} diff --git a/Sources/TuistDependencies/Mappers/PruneOrphanExternalTargetsGraphMapper.swift b/Sources/TuistDependencies/Mappers/PruneOrphanExternalTargetsGraphMapper.swift new file mode 100644 index 00000000000..923c50a166b --- /dev/null +++ b/Sources/TuistDependencies/Mappers/PruneOrphanExternalTargetsGraphMapper.swift @@ -0,0 +1,33 @@ +import Foundation +import TuistCore +import TuistGraph + +/** + External dependencies might contain targets that are only relevant in development, but that + that are not necessary when the dependencies are consumed downstream by Tuist projects. + This graph mappers detects and prunes those targets + */ +public struct PruneOrphanExternalTargetsGraphMapper: GraphMapping { + public init() {} + + public func map(graph: TuistGraph.Graph) async throws -> (TuistGraph.Graph, [TuistCore.SideEffectDescriptor]) { + let graphTraverser = GraphTraverser(graph: graph) + let orphanExternalTargets = graphTraverser.allOrphanExternalTargets() + + var graph = graph + graph.targets = Dictionary(uniqueKeysWithValues: graph.targets.map { projectPath, targets in + let targets = Dictionary(uniqueKeysWithValues: targets.compactMap { targetName, target -> (String, Target)? in + let project = graph.projects[projectPath]! + let graphTarget = GraphTarget(path: projectPath, target: target, project: project) + if orphanExternalTargets.contains(graphTarget) { + return nil + } else { + return (targetName, target) + } + }) + return (projectPath, targets) + }) + + return (graph, []) + } +} diff --git a/Sources/TuistDependencies/SwiftPackageManager/Utils/PackageInfoMapper.swift b/Sources/TuistDependencies/SwiftPackageManager/Utils/PackageInfoMapper.swift index 2153ed6ffad..842dcb91209 100644 --- a/Sources/TuistDependencies/SwiftPackageManager/Utils/PackageInfoMapper.swift +++ b/Sources/TuistDependencies/SwiftPackageManager/Utils/PackageInfoMapper.swift @@ -265,7 +265,7 @@ public final class PackageInfoMapper: PackageInfoMapping { var externalDependencies: [String: [ProjectDescription.TargetDependency]] = .init() externalDependencies = try packageInfos .reduce(into: [:]) { result, packageInfo in - try packageInfo.value.products.forEach { product in + for product in packageInfo.value.products { result[product.name] = try product.targets.flatMap { target in try ResolvedDependency.fromTarget( name: target, @@ -299,7 +299,7 @@ public final class PackageInfoMapper: PackageInfoMapping { let targetToModuleMap: [String: ModuleMap] targetToModuleMap = try packageInfos.reduce(into: [:]) { result, packageInfo in - try packageInfo.value.targets.forEach { target in + for target in packageInfo.value.targets { switch target.type { case .system: /// System library targets assume the module map is located at the source directory root @@ -322,7 +322,7 @@ public final class PackageInfoMapper: PackageInfoMapping { publicHeadersPath: target.publicHeadersPath(packageFolder: packageToFolder[packageInfo.key]!) ) default: - return + continue } } } @@ -354,7 +354,7 @@ public final class PackageInfoMapper: PackageInfoMapping { packageInfos: [String: PackageInfo] ) -> Set { let targetTypes = packageInfos.reduce(into: [String: PackageInfo.Target.TargetType]()) { partialResult, item in - item.value.targets.forEach { target in + for target in item.value.targets { partialResult[target.name] = target.type } } @@ -432,6 +432,7 @@ public final class PackageInfoMapper: PackageInfoMapping { "TempuraTesting", // https://github.com/BendingSpoons/tempura-swift "TSCTestSupport", // https://github.com/apple/swift-tools-support-core "ViewInspector", // https://github.com/nalexn/ViewInspector + "XCTVapor", // https://github.com/vapor/vapor ].map { ($0, ["ENABLE_TESTING_SEARCH_PATHS": "YES"]) } @@ -537,12 +538,6 @@ extension ProjectDescription.Target { var destinations: ProjectDescription.Destinations if target.type == .macro { destinations = Set([.mac]) - } else if packageName == "Firebase" { - // Some firebase targets need certain platforms removed in order for caching to work corractly - let supportedPlatforms = try ProjectDescription.Destinations.fromFirebase( - targetNamed: target.name - ) - destinations = packageDestinations.intersection(supportedPlatforms) } else { // All packages implicitly support all platforms, we constrain this with the platforms defined in `Dependencies.swift` destinations = packageDestinations.intersection(Set(Destination.allCases)) @@ -865,36 +860,6 @@ extension ResourceFileElements { } } -extension ProjectDescription.Destinations { - fileprivate static func fromFirebase(targetNamed name: String) throws -> Self { - let platforms = ProjectDescription.PackagePlatform.allCases - return Set( - try platforms.filter { platform in - switch name { - case "FirebaseAnalyticsWrapper", - "FirebaseAnalyticsSwift", - "FirebaseAnalyticsWithoutAdIdSupportWrapper", - "FirebaseFirestoreSwift", - "FirebaseFirestore": // These dont support watchOS - platform != .watchOS - case "FirebaseAppDistribution", - "FirebaseDynamicLinks": // iOS only - platform == .iOS - case "FirebaseInAppMessaging": - platform == .iOS || - platform == .tvOS || - platform == .visionOS - case "FirebasePerformance": - platform != .macOS && - platform != .watchOS - default: true - } - } - .flatMap { try $0.destinations() } - ) - } -} - extension ProjectDescription.TargetDependency { fileprivate static func from( resolvedDependencies: [PackageInfoMapper.ResolvedDependency], @@ -1352,7 +1317,7 @@ extension PackageInfoMapper { targetDependencyToFramework: targetDependencyToFramework, condition: condition ) - case let .product(name, package, condition): + case let .product(name, package, _, condition): return try Self.fromProduct( package: idToPackage[package.lowercased()] ?? package, product: name, diff --git a/Sources/TuistDependencies/SwiftPackageManager/Utils/SettingsMapper.swift b/Sources/TuistDependencies/SwiftPackageManager/Utils/SettingsMapper.swift index 1a054485d99..e3d7bb0dd91 100644 --- a/Sources/TuistDependencies/SwiftPackageManager/Utils/SettingsMapper.swift +++ b/Sources/TuistDependencies/SwiftPackageManager/Utils/SettingsMapper.swift @@ -32,6 +32,7 @@ struct SettingsMapper { } } + // swiftlint:disable:next function_body_length func settingsDictionaryForPlatform(_ platform: PackageInfo.Platform?) throws -> TuistGraph.SettingsDictionary { var headerSearchPaths = headerSearchPaths var defines = ["SWIFT_PACKAGE": "1"] @@ -44,7 +45,7 @@ struct SettingsMapper { var settingsDictionary = TuistGraph.SettingsDictionary() let platformSettings = try settingsForPlatform(platform?.platformName) - try platformSettings.forEach { setting in + for setting in platformSettings { switch (setting.tool, setting.name) { case (.c, .headerSearchPath), (.cxx, .headerSearchPath): headerSearchPaths.append("$(SRCROOT)/\(mainRelativePath.pathString)/\(setting.value[0])") @@ -67,7 +68,7 @@ struct SettingsMapper { linkerFlags.append(contentsOf: setting.value) case (.linker, .linkedFramework), (.linker, .linkedLibrary): // Handled as dependency - return + continue case (.c, .linkedFramework), (.c, .linkedLibrary), (.cxx, .linkedFramework), (.cxx, .linkedLibrary), (.swift, .headerSearchPath), (.swift, .linkedFramework), (.swift, .linkedLibrary), diff --git a/Sources/TuistDependenciesTesting/DependenciesGraph/DependenciesGraph+TestData.swift b/Sources/TuistDependenciesTesting/DependenciesGraph/DependenciesGraph+TestData.swift index de76b3a3670..12f57f38e36 100644 --- a/Sources/TuistDependenciesTesting/DependenciesGraph/DependenciesGraph+TestData.swift +++ b/Sources/TuistDependenciesTesting/DependenciesGraph/DependenciesGraph+TestData.swift @@ -232,7 +232,6 @@ extension TuistCore.DependenciesGraph { ) } - // swiftlint:disable:next function_body_length public static func anotherDependency( spmFolder: Path, destinations: Destinations = [.iPhone, .iPad, .macWithiPadDesign, .appleVisionWithiPadDesign] diff --git a/Sources/TuistDependenciesTesting/MockDependenciesController.swift b/Sources/TuistDependenciesTesting/MockDependenciesController.swift index 7e035bf2d02..78b2f7bfb77 100644 --- a/Sources/TuistDependenciesTesting/MockDependenciesController.swift +++ b/Sources/TuistDependenciesTesting/MockDependenciesController.swift @@ -10,7 +10,10 @@ public final class MockDependenciesController: DependenciesControlling { public init() {} public var invokedFetch = false - public var fetchStub: ((AbsolutePath, TuistGraph.Dependencies, TSCUtility.Version?) throws -> TuistCore.DependenciesGraph)? + public var legacyFetchStub: ( + (AbsolutePath, TuistGraph.Dependencies, TSCUtility.Version?) throws -> TuistCore + .DependenciesGraph + )? public func fetch( at path: AbsolutePath, @@ -18,11 +21,24 @@ public final class MockDependenciesController: DependenciesControlling { swiftVersion: TSCUtility.Version? ) throws -> TuistCore.DependenciesGraph { invokedFetch = true - return try fetchStub?(path, dependencies, swiftVersion) ?? .none + return try legacyFetchStub?(path, dependencies, swiftVersion) ?? .none + } + + public var fetchStub: ((AbsolutePath, TuistGraph.PackageSettings, TSCUtility.Version?) throws -> TuistCore.DependenciesGraph)? + public func fetch( + at path: AbsolutePath, + packageSettings: TuistGraph.PackageSettings, + swiftVersion: TSCUtility.Version? + ) throws -> TuistCore.DependenciesGraph { + invokedFetch = true + return try fetchStub?(path, packageSettings, swiftVersion) ?? .none } public var invokedUpdate = false - public var updateStub: ((AbsolutePath, TuistGraph.Dependencies, TSCUtility.Version?) throws -> TuistCore.DependenciesGraph)? + public var legacyUpdateStub: ( + (AbsolutePath, TuistGraph.Dependencies, TSCUtility.Version?) throws -> TuistCore + .DependenciesGraph + )? public func update( at path: AbsolutePath, @@ -30,7 +46,21 @@ public final class MockDependenciesController: DependenciesControlling { swiftVersion: TSCUtility.Version? ) throws -> TuistCore.DependenciesGraph { invokedUpdate = true - return try updateStub?(path, dependencies, swiftVersion) ?? .none + return try legacyUpdateStub?(path, dependencies, swiftVersion) ?? .none + } + + public var updateStub: ( + (AbsolutePath, TuistGraph.PackageSettings, TSCUtility.Version?) throws -> TuistCore + .DependenciesGraph + )? + + public func update( + at path: AbsolutePath, + packageSettings: TuistGraph.PackageSettings, + swiftVersion: TSCUtility.Version? + ) throws -> TuistCore.DependenciesGraph { + invokedUpdate = true + return try updateStub?(path, packageSettings, swiftVersion) ?? .none } public var invokedSave = false diff --git a/Sources/TuistEnvKit/Commands/TuistCommand.swift b/Sources/TuistEnvKit/Commands/TuistCommand.swift index 270956543e7..8a05a6d0541 100644 --- a/Sources/TuistEnvKit/Commands/TuistCommand.swift +++ b/Sources/TuistEnvKit/Commands/TuistCommand.swift @@ -53,11 +53,14 @@ public struct TuistCommand: ParsableCommand { } else { try CommandRunner().run() } + WarningController.shared.flush() _exit(0) } catch let error as FatalError { + WarningController.shared.flush() errorHandler.fatal(error: error) _exit(exitCode(for: error).rawValue) } catch { + WarningController.shared.flush() // Exit cleanly if exitCode(for: error).rawValue == 0 { exit(withError: error) diff --git a/Sources/TuistEnvKit/Log/Logger.swift b/Sources/TuistEnvKit/Log/Logger.swift index e51face1a6d..6526fbc1b8f 100644 --- a/Sources/TuistEnvKit/Log/Logger.swift +++ b/Sources/TuistEnvKit/Log/Logger.swift @@ -1,2 +1,3 @@ import TuistSupport + let logger = Logger(label: "io.tuist.env") diff --git a/Sources/TuistGenerator/Extensions/Graph+Extras.swift b/Sources/TuistGenerator/Extensions/Graph+Extras.swift index dcac0708333..7b15a196424 100644 --- a/Sources/TuistGenerator/Extensions/Graph+Extras.swift +++ b/Sources/TuistGenerator/Extensions/Graph+Extras.swift @@ -35,7 +35,7 @@ extension TuistGraph.Graph { let filteredTargetsAndDependencies: Set = filteredTargets.union( transitiveClosure(Array(filteredTargets)) { target in - Array(graphTraverser.directTargetDependencies(path: target.path, name: target.target.name)) + Array(graphTraverser.directTargetDependencies(path: target.path, name: target.target.name).map(\.graphTarget)) } ) @@ -59,7 +59,7 @@ extension GraphDependency { switch self { case let .target(_, path): return projects[path]?.isExternal ?? false - case .framework, .xcframework, .library, .bundle, .packageProduct, .sdk: + case .framework, .xcframework, .library, .bundle, .packageProduct, .sdk, .macro: return true } } diff --git a/Sources/TuistGenerator/Generator/BuildPhaseGenerator.swift b/Sources/TuistGenerator/Generator/BuildPhaseGenerator.swift index 455f01073e1..4a4f86045c1 100644 --- a/Sources/TuistGenerator/Generator/BuildPhaseGenerator.swift +++ b/Sources/TuistGenerator/Generator/BuildPhaseGenerator.swift @@ -169,12 +169,18 @@ final class BuildPhaseGenerator: BuildPhaseGenerating { pbxproj: PBXProj, sourceRootPath: AbsolutePath ) throws { - try scripts.forEach { script in + for script in scripts { let buildPhase = try PBXShellScriptBuildPhase( files: [], name: script.name, - inputPaths: script.inputPaths.map { $0.relative(to: sourceRootPath).pathString }, - outputPaths: script.outputPaths.map { $0.relative(to: sourceRootPath).pathString }, + inputPaths: script.inputPaths + .map { + (try? AbsolutePath(validating: $0))?.relative(to: sourceRootPath).pathString ?? $0 + }, + outputPaths: script.outputPaths + .map { + (try? AbsolutePath(validating: $0))?.relative(to: sourceRootPath).pathString ?? $0 + }, inputFileListPaths: script.inputFileListPaths.map { $0.relative(to: sourceRootPath).pathString }, outputFileListPaths: script.outputFileListPaths.map { $0.relative(to: sourceRootPath).pathString }, @@ -202,7 +208,7 @@ final class BuildPhaseGenerator: BuildPhaseGenerating { pbxTarget: PBXTarget, pbxproj: PBXProj ) { - rawScriptBuildPhases.forEach { script in + for script in rawScriptBuildPhases { let buildPhase = PBXShellScriptBuildPhase( files: [], name: script.name, @@ -215,6 +221,7 @@ final class BuildPhaseGenerator: BuildPhaseGenerating { } } + // swiftlint:disable:next function_body_length func generateSourcesBuildPhase( files: [SourceFile], coreDataModels: [CoreDataModel], @@ -232,7 +239,7 @@ final class BuildPhaseGenerator: BuildPhaseGenerating { .sorted(by: { $0.path < $1.path }) var pbxBuildFiles = [PBXBuildFile]() - try sortedFiles.forEach { buildFile in + for buildFile in sortedFiles { let buildFilePath = buildFile.path let isLocalized = buildFilePath.pathString.contains(".lproj/") let element: (element: PBXFileElement, path: AbsolutePath) @@ -368,7 +375,7 @@ final class BuildPhaseGenerator: BuildPhaseGenerating { fileElements: ProjectFileElements, pbxproj: PBXProj ) throws { - try target.copyFiles.forEach { action in + for action in target.copyFiles { let copyFilesPhase = PBXCopyFilesBuildPhase( dstPath: action.subpath, dstSubfolderSpec: action.destination.toXcodeprojSubFolder, @@ -382,15 +389,15 @@ final class BuildPhaseGenerator: BuildPhaseGenerating { let filePaths = action.files.map(\.path).sorted() var pbxBuildFiles = [PBXBuildFile]() - try filePaths.forEach { - guard let fileReference = fileElements.file(path: $0) else { - throw BuildPhaseGenerationError.missingFileReference($0) + for filePath in filePaths { + guard let fileReference = fileElements.file(path: filePath) else { + throw BuildPhaseGenerationError.missingFileReference(filePath) } - if buildFilesCache.contains($0) == false { + if buildFilesCache.contains(filePath) == false { let pbxBuildFile = PBXBuildFile(file: fileReference) pbxBuildFiles.append(pbxBuildFile) - buildFilesCache.insert($0) + buildFilesCache.insert(filePath) } } pbxBuildFiles.forEach { pbxproj.add(object: $0) } @@ -407,7 +414,7 @@ final class BuildPhaseGenerator: BuildPhaseGenerating { var pbxBuildFiles = [PBXBuildFile]() let ignoredVariantGroupExtensions = [".intentdefinition"] - try files.sorted(by: { $0.path < $1.path }).forEach { resource in + for resource in files.sorted(by: { $0.path < $1.path }) { let buildFilePath = resource.path let pathString = buildFilePath.pathString @@ -424,7 +431,7 @@ final class BuildPhaseGenerator: BuildPhaseGenerating { // Xcode automatically copies the string files for some files (i.e. .intentdefinition files), hence we need // to remove them from the Copy Resources build phase if let suffix = path.suffix, ignoredVariantGroupExtensions.contains(suffix) { - return + continue } element = (group, path) @@ -516,18 +523,19 @@ final class BuildPhaseGenerator: BuildPhaseGenerating { ) throws { let appExtensions = graphTraverser.appExtensionDependencies(path: path, name: target.name).sorted() guard !appExtensions.isEmpty else { return } - var pbxBuildFiles = [PBXBuildFile]() let appExtensionsBuildPhase = PBXCopyFilesBuildPhase(dstSubfolderSpec: .plugins, name: "Embed Foundation Extensions") pbxproj.add(object: appExtensionsBuildPhase) pbxTarget.buildPhases.append(appExtensionsBuildPhase) - let refs = appExtensions.compactMap { fileElements.product(target: $0.target.name) } + let pbxBuildFiles: [PBXBuildFile] = appExtensions.compactMap { extensionTargetReference in + guard let fileReference = fileElements.product(target: extensionTargetReference.target.name) else { return nil } - refs.forEach { - let pbxBuildFile = PBXBuildFile(file: $0, settings: ["ATTRIBUTES": ["RemoveHeadersOnCopy"]]) - pbxBuildFiles.append(pbxBuildFile) + let pbxBuildFile = PBXBuildFile(file: fileReference, settings: ["ATTRIBUTES": ["RemoveHeadersOnCopy"]]) + pbxBuildFile.applyCondition(extensionTargetReference.condition, applicableTo: target) + return pbxBuildFile } + pbxBuildFiles.forEach { pbxproj.add(object: $0) } appExtensionsBuildPhase.files = pbxBuildFiles } @@ -543,7 +551,6 @@ final class BuildPhaseGenerator: BuildPhaseGenerating { let targetDependencies = graphTraverser.directLocalTargetDependencies(path: path, name: target.name).sorted() let watchApps = targetDependencies.filter { $0.target.isEmbeddableWatchApplication() } guard !watchApps.isEmpty else { return } - var pbxBuildFiles = [PBXBuildFile]() let embedWatchAppBuildPhase = PBXCopyFilesBuildPhase( dstPath: "$(CONTENTS_FOLDER_PATH)/Watch", @@ -553,12 +560,14 @@ final class BuildPhaseGenerator: BuildPhaseGenerating { pbxproj.add(object: embedWatchAppBuildPhase) pbxTarget.buildPhases.append(embedWatchAppBuildPhase) - let refs = watchApps.compactMap { fileElements.product(target: $0.target.name) } + let pbxBuildFiles: [PBXBuildFile] = watchApps.compactMap { appTargetReference in + guard let fileReference = fileElements.product(target: appTargetReference.target.name) else { return nil } - refs.forEach { - let pbxBuildFile = PBXBuildFile(file: $0, settings: ["ATTRIBUTES": ["RemoveHeadersOnCopy"]]) - pbxBuildFiles.append(pbxBuildFile) + let pbxBuildFile = PBXBuildFile(file: fileReference, settings: ["ATTRIBUTES": ["RemoveHeadersOnCopy"]]) + pbxBuildFile.applyCondition(appTargetReference.condition, applicableTo: target) + return pbxBuildFile } + pbxBuildFiles.forEach { pbxproj.add(object: $0) } embedWatchAppBuildPhase.files = pbxBuildFiles } @@ -575,7 +584,7 @@ final class BuildPhaseGenerator: BuildPhaseGenerating { return } - guard let appClips = graphTraverser.appClipDependencies(path: path, name: target.name) else { + guard let appClipTargetReference = graphTraverser.appClipDependencies(path: path, name: target.name) else { return } var pbxBuildFiles = [PBXBuildFile]() @@ -588,9 +597,10 @@ final class BuildPhaseGenerator: BuildPhaseGenerating { pbxproj.add(object: embedAppClipsBuildPhase) pbxTarget.buildPhases.append(embedAppClipsBuildPhase) - let refs = fileElements.product(target: appClips.target.name) + let refs = fileElements.product(target: appClipTargetReference.target.name) let pbxBuildFile = PBXBuildFile(file: refs, settings: ["ATTRIBUTES": ["RemoveHeadersOnCopy"]]) + pbxBuildFile.applyCondition(appClipTargetReference.condition, applicableTo: target) pbxBuildFiles.append(pbxBuildFile) pbxBuildFiles.forEach { pbxproj.add(object: $0) } embedAppClipsBuildPhase.files = pbxBuildFiles @@ -607,7 +617,6 @@ final class BuildPhaseGenerator: BuildPhaseGenerating { let targetDependencies = graphTraverser.directLocalTargetDependencies(path: path, name: target.name).sorted() let xpcServices = targetDependencies.filter { $0.target.isEmbeddableXPCService() } guard !xpcServices.isEmpty else { return } - var pbxBuildFiles = [PBXBuildFile]() let embedXPCServicesBuildPhase = PBXCopyFilesBuildPhase( dstPath: "$(CONTENTS_FOLDER_PATH)/XPCServices", @@ -617,15 +626,16 @@ final class BuildPhaseGenerator: BuildPhaseGenerating { pbxproj.add(object: embedXPCServicesBuildPhase) pbxTarget.buildPhases.append(embedXPCServicesBuildPhase) - let refs = xpcServices.compactMap { fileElements.product(target: $0.target.name) } + let pbxBuildFiles: [PBXBuildFile] = xpcServices.compactMap { graphTarget in + guard let fileReference = fileElements.product(target: graphTarget.target.name) else { return nil } - refs.forEach { - let pbxBuildFile = PBXBuildFile(file: $0, settings: ["ATTRIBUTES": ["RemoveHeadersOnCopy"]]) - if !target.isExclusiveTo(.macOS) { + let pbxBuildFile = PBXBuildFile(file: fileReference, settings: ["ATTRIBUTES": ["RemoveHeadersOnCopy"]]) + if !graphTarget.target.isExclusiveTo(.macOS) { pbxBuildFile.applyPlatformFilters([.macos]) } - pbxBuildFiles.append(pbxBuildFile) + return pbxBuildFile } + pbxBuildFiles.forEach { pbxproj.add(object: $0) } embedXPCServicesBuildPhase.files = pbxBuildFiles } @@ -641,7 +651,6 @@ final class BuildPhaseGenerator: BuildPhaseGenerating { let targetDependencies = graphTraverser.directLocalTargetDependencies(path: path, name: target.name).sorted() let systemExtensions = targetDependencies.filter { $0.target.isEmbeddableSystemExtension() } guard !systemExtensions.isEmpty else { return } - var pbxBuildFiles = [PBXBuildFile]() let embedSystemExtensionsBuildPhase = PBXCopyFilesBuildPhase( dstPath: "$(CONTENTS_FOLDER_PATH)/Library/SystemExtensions", @@ -651,12 +660,16 @@ final class BuildPhaseGenerator: BuildPhaseGenerating { pbxproj.add(object: embedSystemExtensionsBuildPhase) pbxTarget.buildPhases.append(embedSystemExtensionsBuildPhase) - let refs = systemExtensions.compactMap { fileElements.product(target: $0.target.name) } + let pbxBuildFiles: [PBXBuildFile] = systemExtensions.compactMap { graphTarget in + guard let fileReference = fileElements.product(target: graphTarget.target.name) else { return nil } - refs.forEach { - let pbxBuildFile = PBXBuildFile(file: $0, settings: ["ATTRIBUTES": ["RemoveHeadersOnCopy"]]) - pbxBuildFiles.append(pbxBuildFile) + let pbxBuildFile = PBXBuildFile(file: fileReference, settings: ["ATTRIBUTES": ["RemoveHeadersOnCopy"]]) + if !graphTarget.target.isExclusiveTo(.macOS) { + pbxBuildFile.applyPlatformFilters([.macos]) + } + return pbxBuildFile } + pbxBuildFiles.forEach { pbxproj.add(object: $0) } embedSystemExtensionsBuildPhase.files = pbxBuildFiles } @@ -671,7 +684,6 @@ final class BuildPhaseGenerator: BuildPhaseGenerating { ) throws { let extensions = graphTraverser.extensionKitExtensionDependencies(path: path, name: target.name).sorted() guard !extensions.isEmpty else { return } - var pbxBuildFiles = [PBXBuildFile]() let extensionKitExtensionsBuildPhase = PBXCopyFilesBuildPhase( dstPath: "$(EXTENSIONS_FOLDER_PATH)", @@ -681,11 +693,14 @@ final class BuildPhaseGenerator: BuildPhaseGenerating { pbxproj.add(object: extensionKitExtensionsBuildPhase) pbxTarget.buildPhases.append(extensionKitExtensionsBuildPhase) - let refs = extensions.compactMap { fileElements.product(target: $0.target.name) } - refs.forEach { - let pbxBuildFile = PBXBuildFile(file: $0, settings: ["ATTRIBUTES": ["RemoveHeadersOnCopy"]]) - pbxBuildFiles.append(pbxBuildFile) + let pbxBuildFiles: [PBXBuildFile] = extensions.compactMap { extensionTargetReference in + guard let fileReference = fileElements.product(target: extensionTargetReference.target.name) else { return nil } + + let pbxBuildFile = PBXBuildFile(file: fileReference, settings: ["ATTRIBUTES": ["RemoveHeadersOnCopy"]]) + pbxBuildFile.applyCondition(extensionTargetReference.condition, applicableTo: target) + return pbxBuildFile } + pbxBuildFiles.forEach { pbxproj.add(object: $0) } extensionKitExtensionsBuildPhase.files = pbxBuildFiles } diff --git a/Sources/TuistGenerator/Generator/BuildRulesGenerator.swift b/Sources/TuistGenerator/Generator/BuildRulesGenerator.swift index 9044f3870b6..6fb40f61fbb 100644 --- a/Sources/TuistGenerator/Generator/BuildRulesGenerator.swift +++ b/Sources/TuistGenerator/Generator/BuildRulesGenerator.swift @@ -8,18 +8,18 @@ protocol BuildRulesGenerating: AnyObject { final class BuildRulesGenerator: BuildRulesGenerating { func generateBuildRules(target: Target, pbxTarget: PBXTarget, pbxproj: PBXProj) throws { - target.buildRules.forEach { + for buildRule in target.buildRules { let rule = PBXBuildRule( - compilerSpec: $0.compilerSpec.rawValue, - fileType: $0.fileType.rawValue, + compilerSpec: buildRule.compilerSpec.rawValue, + fileType: buildRule.fileType.rawValue, isEditable: true, - filePatterns: $0.filePatterns, - name: $0.name, - outputFiles: $0.outputFiles, - inputFiles: $0.inputFiles, - outputFilesCompilerFlags: $0.outputFilesCompilerFlags, - script: $0.script, - runOncePerArchitecture: $0.runOncePerArchitecture + filePatterns: buildRule.filePatterns, + name: buildRule.name, + outputFiles: buildRule.outputFiles, + inputFiles: buildRule.inputFiles, + outputFilesCompilerFlags: buildRule.outputFilesCompilerFlags, + script: buildRule.script, + runOncePerArchitecture: buildRule.runOncePerArchitecture ) pbxTarget.buildRules.append(rule) pbxproj.add(object: rule) diff --git a/Sources/TuistGenerator/Generator/ConfigGenerator.swift b/Sources/TuistGenerator/Generator/ConfigGenerator.swift index 174f21791df..0dcf2abfff7 100644 --- a/Sources/TuistGenerator/Generator/ConfigGenerator.swift +++ b/Sources/TuistGenerator/Generator/ConfigGenerator.swift @@ -57,10 +57,10 @@ final class ConfigGenerator: ConfigGenerating { ) pbxproj.add(object: configurationList) - try project.settings.configurations.sortedByBuildConfigurationName().forEach { + for item in project.settings.configurations.sortedByBuildConfigurationName() { try generateProjectSettingsFor( - buildConfiguration: $0.key, - configuration: $0.value, + buildConfiguration: item.key, + configuration: item.value, project: project, fileElements: fileElements, pbxproj: pbxproj, @@ -104,12 +104,12 @@ final class ConfigGenerator: ConfigGenerating { let configurations = Dictionary(uniqueKeysWithValues: configurationsTuples) let nonEmptyConfigurations = !configurations.isEmpty ? configurations : Settings.default.configurations let orderedConfigurations = nonEmptyConfigurations.sortedByBuildConfigurationName() - try orderedConfigurations.forEach { + for orderedConfiguration in orderedConfigurations { try generateTargetSettingsFor( target: target, project: project, - buildConfiguration: $0.key, - configuration: $0.value, + buildConfiguration: orderedConfiguration.key, + configuration: orderedConfiguration.value, fileElements: fileElements, graphTraverser: graphTraverser, pbxproj: pbxproj, @@ -181,6 +181,9 @@ final class ConfigGenerator: ConfigGenerating { ) settingsHelper.extend(buildSettings: &settings, with: target.settings?.base ?? [:]) + if buildConfiguration.variant == .debug { + settingsHelper.extend(buildSettings: &settings, with: target.settings?.baseDebug ?? [:]) + } settingsHelper.extend(buildSettings: &settings, with: configuration?.settings ?? [:]) let variantBuildConfiguration = XCBuildConfiguration( @@ -312,23 +315,11 @@ final class ConfigGenerator: ConfigGenerating { graphTraverser: GraphTraversing, projectPath: AbsolutePath ) -> SettingsDictionary { - let targets = graphTraverser.directSwiftMacroFrameworkTargets(path: projectPath, name: target.name) - if targets.isEmpty { return [:] } + let pluginExecutables = graphTraverser.allSwiftPluginExecutables(path: projectPath, name: target.name) var settings: SettingsDictionary = [:] - settings["OTHER_SWIFT_FLAGS"] = .array(targets.flatMap { target in - let macroExecutables = graphTraverser.directSwiftMacroExecutables(path: target.path, name: target.target.name) - return macroExecutables.flatMap { macroExecutable in - switch macroExecutable { - case let .product(_, productName, _): - return [ - "-load-plugin-executable", - "$BUILT_PRODUCTS_DIR/\(target.target.productNameWithExtension)/Macros/\(productName)#\(productName)", - ] - default: - return [] - } - } - }) + if pluginExecutables.isEmpty { return settings } + let swiftCompilerFlags = pluginExecutables.flatMap { ["-load-plugin-executable", $0] } + settings["OTHER_SWIFT_FLAGS"] = .array(swiftCompilerFlags) return settings } diff --git a/Sources/TuistGenerator/Generator/LinkGenerator.swift b/Sources/TuistGenerator/Generator/LinkGenerator.swift index cac62ac2990..ec3749ed557 100644 --- a/Sources/TuistGenerator/Generator/LinkGenerator.swift +++ b/Sources/TuistGenerator/Generator/LinkGenerator.swift @@ -122,7 +122,7 @@ final class LinkGenerator: LinkGenerating { // swiftlint:disable:this type_body_ Target -> MyMacro (Static framework) -> MyMacro (Executable) The executable is compiled transitively through the static library, and we place it inside the framework to make it available to the target depending on the framework - to point it with the `-load-plugin-executable $BUILT_PRODUCTS_DIR/ExecutableName\#ExecutableName` build setting. + to point it with the `-load-plugin-executable $(BUILD_DIR)/$(CONFIGURATION)/ExecutableName\#ExecutableName` build setting. */ let directSwiftMacroExecutables = graphTraverser.directSwiftMacroExecutables(path: path, name: target.name).sorted() try generateCopySwiftMacroExecutableScriptBuildPhase( @@ -229,7 +229,7 @@ final class LinkGenerator: LinkGenerating { // swiftlint:disable:this type_body_ var frameworkReferences: [GraphDependencyReference] = [] - try embeddableFrameworks.forEach { dependency in + for dependency in embeddableFrameworks { switch dependency { case .framework: frameworkReferences.append(dependency) @@ -255,7 +255,7 @@ final class LinkGenerator: LinkGenerating { // swiftlint:disable:this type_body_ buildFile.applyCondition(condition, applicableTo: target) pbxproj.add(object: buildFile) embedPhase.files?.append(buildFile) - case .library, .bundle, .sdk: + case .library, .bundle, .sdk, .macro: // Do nothing break } @@ -391,7 +391,7 @@ final class LinkGenerator: LinkGenerating { // swiftlint:disable:this type_body_ .array(["$(inherited)"] + paths.map { $0.xcodeValue(sourceRootPath: sourceRootPath) }.uniqued().sorted()) let newSetting = [name: value] let helper = SettingsHelper() - try configurationList.buildConfigurations.forEach { configuration in + for configuration in configurationList.buildConfigurations { try helper.extend(buildSettings: &configuration.buildSettings, with: newSetting) } } @@ -428,36 +428,35 @@ final class LinkGenerator: LinkGenerating { // swiftlint:disable:this type_body_ buildPhase.files?.append(buildFile) } - try linkableDependencies - .forEach { dependency in - switch dependency { - case let .framework(path, _, _, _, _, _, _, _, status, condition): - try addBuildFile(path, condition: condition, status: status) - case let .library(path, _, _, _, condition): - try addBuildFile(path, condition: condition) - case let .xcframework(path, _, _, _, status, condition): - try addBuildFile(path, condition: condition, status: status) - case .bundle: - break - case let .product(dependencyTarget, _, condition): - guard let fileRef = fileElements.product(target: dependencyTarget) else { - throw LinkGeneratorError.missingProduct(name: dependencyTarget) - } - let buildFile = PBXBuildFile(file: fileRef) - buildFile.applyCondition(condition, applicableTo: target) - pbxproj.add(object: buildFile) - buildPhase.files?.append(buildFile) - case let .sdk(sdkPath, sdkStatus, _, condition): - guard let fileRef = fileElements.sdk(path: sdkPath) else { - throw LinkGeneratorError.missingReference(path: sdkPath) - } - - let buildFile = createSDKBuildFile(for: fileRef, status: sdkStatus) - buildFile.applyCondition(condition, applicableTo: target) - pbxproj.add(object: buildFile) - buildPhase.files?.append(buildFile) + for dependency in linkableDependencies { + switch dependency { + case let .framework(path, _, _, _, _, _, _, _, status, condition): + try addBuildFile(path, condition: condition, status: status) + case let .library(path, _, _, _, condition): + try addBuildFile(path, condition: condition) + case let .xcframework(path, _, _, _, status, condition): + try addBuildFile(path, condition: condition, status: status) + case .bundle, .macro: + break + case let .product(dependencyTarget, _, condition): + guard let fileRef = fileElements.product(target: dependencyTarget) else { + throw LinkGeneratorError.missingProduct(name: dependencyTarget) } + let buildFile = PBXBuildFile(file: fileRef) + buildFile.applyCondition(condition, applicableTo: target) + pbxproj.add(object: buildFile) + buildPhase.files?.append(buildFile) + case let .sdk(sdkPath, sdkStatus, _, condition): + guard let fileRef = fileElements.sdk(path: sdkPath) else { + throw LinkGeneratorError.missingReference(path: sdkPath) + } + + let buildFile = createSDKBuildFile(for: fileRef, status: sdkStatus) + buildFile.applyCondition(condition, applicableTo: target) + pbxproj.add(object: buildFile) + buildPhase.files?.append(buildFile) } + } } func generateCopyProductsBuildPhase( @@ -492,7 +491,7 @@ final class LinkGenerator: LinkGenerating { // swiftlint:disable:this type_body_ ) // For static framewor/library XCFrameworks, we need Xcode to process it to extract the - // the relevant product within it within it based on the architecture and place in + // the relevant product within it based on the architecture and place in // the products directory. This allows the current target to see the symbols from the XCFramework. // // Copying to products is not a nop like it is for regular static targets due to the processing step, @@ -516,27 +515,54 @@ final class LinkGenerator: LinkGenerating { // swiftlint:disable:this type_body_ func generateCopySwiftMacroExecutableScriptBuildPhase( directSwiftMacroExecutables: [GraphDependencyReference], - target _: Target, + target: Target, pbxTarget: PBXTarget, pbxproj: PBXProj, fileElements _: ProjectFileElements ) throws { if directSwiftMacroExecutables.isEmpty { return } - let copySwiftMacrosBuildPhase = PBXShellScriptBuildPhase(name: "Copy Swift Macro executable into /Macros") + let copySwiftMacrosBuildPhase = PBXShellScriptBuildPhase(name: "Copy Swift Macro executable into $BUILT_PRODUCT_DIR") - let filesToCopy = directSwiftMacroExecutables.compactMap { + let executableNames = directSwiftMacroExecutables.compactMap { switch $0 { case let .product(_, productName, _): return productName default: return nil } - }.map { ("$SYMROOT/$CONFIGURATION/\($0)", "$BUILT_PRODUCTS_DIR/$FULL_PRODUCT_NAME/Macros/\($0)") } + } - copySwiftMacrosBuildPhase.shellScript = filesToCopy.map { "cp \"\($0.0)\" \"\($0.1)\"" }.joined(separator: "\n") - copySwiftMacrosBuildPhase.inputPaths = filesToCopy.map(\.0) - copySwiftMacrosBuildPhase.outputPaths = filesToCopy.map(\.1) + let copyLines = executableNames.map { + """ + if [[ -f "$BUILD_DIR/$CONFIGURATION/\($0)" && ! -f "$BUILD_DIR/Debug$EFFECTIVE_PLATFORM_NAME/\($0)" ]]; then + mkdir -p "$BUILD_DIR/Debug$EFFECTIVE_PLATFORM_NAME/" + cp "$BUILD_DIR/$CONFIGURATION/\($0)" "$BUILD_DIR/Debug$EFFECTIVE_PLATFORM_NAME/\($0)" + fi + """ + } + copySwiftMacrosBuildPhase.shellScript = """ + # This build phase serves two purposes: + # - Force Xcode build system to compile the macOS executable transitively when compiling for non-macOS destinations + # - Place the artifacts in the "Debug" directory where the built artifacts for the active destination live. We default to "Debug" because otherwise the Xcode editor fails to resolve the macro references. + \(copyLines.joined(separator: "\n")) + """ + + copySwiftMacrosBuildPhase.inputPaths = executableNames.map { "$BUILD_DIR/$CONFIGURATION/\($0)" } + + copySwiftMacrosBuildPhase.outputPaths = target.supportedPlatforms + .filter { $0 != .macOS } + .flatMap { platform in + var sdks: [String] = [] + sdks.append(platform.xcodeDeviceSDK) + if let simulatorSDK = platform.xcodeSimulatorSDK { sdks.append(simulatorSDK) } + return sdks + } + .flatMap { sdk in + executableNames.map { executable in + "$BUILD_DIR/Debug-\(sdk)/\(executable)" + } + } pbxproj.add(object: copySwiftMacrosBuildPhase) pbxTarget.buildPhases.append(copySwiftMacrosBuildPhase) diff --git a/Sources/TuistGenerator/Generator/ProjectDescriptorGenerator.swift b/Sources/TuistGenerator/Generator/ProjectDescriptorGenerator.swift index 7b4196aa922..69068b2b662 100644 --- a/Sources/TuistGenerator/Generator/ProjectDescriptorGenerator.swift +++ b/Sources/TuistGenerator/Generator/ProjectDescriptorGenerator.swift @@ -196,7 +196,7 @@ final class ProjectDescriptorGenerator: ProjectDescriptorGenerating { graphTraverser: GraphTraversing ) throws -> [String: PBXNativeTarget] { var nativeTargets: [String: PBXNativeTarget] = [:] - try project.targets.forEach { target in + for target in project.targets { let nativeTarget = try targetGenerator.generateTarget( target: target, project: project, diff --git a/Sources/TuistGenerator/Generator/ProjectFileElements.swift b/Sources/TuistGenerator/Generator/ProjectFileElements.swift index 9cc4c91ef3b..4c3dcb345dc 100644 --- a/Sources/TuistGenerator/Generator/ProjectFileElements.swift +++ b/Sources/TuistGenerator/Generator/ProjectFileElements.swift @@ -55,7 +55,7 @@ class ProjectFileElements { ) throws { var files = Set() - try project.targets.forEach { target in + for target in project.targets { try files.formUnion(targetFiles(target: target)) } let projectFileElements = projectFiles(project: project) @@ -176,8 +176,8 @@ class ProjectFileElements { ) }) - target.copyFiles.forEach { - elements.formUnion($0.files.map { + for copyFile in target.copyFiles { + elements.formUnion(copyFile.files.map { GroupFileElement( path: $0.path, group: target.filesGroup, @@ -195,8 +195,8 @@ class ProjectFileElements { pbxproj: PBXProj, sourceRootPath: AbsolutePath ) throws { - try files.forEach { - try generate(fileElement: $0, groups: groups, pbxproj: pbxproj, sourceRootPath: sourceRootPath) + for file in files { + try generate(fileElement: file, groups: groups, pbxproj: pbxproj, sourceRootPath: sourceRootPath) } } @@ -207,8 +207,16 @@ class ProjectFileElements { sourceRootPath: AbsolutePath, filesGroup: ProjectGroup ) throws { - try dependencyReferences.sorted().forEach { (dependency: GraphDependencyReference) in + for dependency in dependencyReferences.sorted() { switch dependency { + case let .macro(path): + try generatePrecompiledDependency( + path, + groups: groups, + pbxproj: pbxproj, + group: filesGroup, + sourceRootPath: sourceRootPath + ) case let .xcframework(path, _, _, _, _, _): try generatePrecompiledDependency( path, @@ -244,7 +252,7 @@ class ProjectFileElements { case let .sdk(sdkNodePath, _, _, _): generateSDKFileElement( sdkNodePath: sdkNodePath, - toGroup: groups.compiled, + toGroup: groups.frameworks, pbxproj: pbxproj ) case let .product(target: target, productName: productName, _): @@ -274,7 +282,7 @@ class ProjectFileElements { from: sourceRootPath, fileAbsolutePath: path, name: path.basename, - toGroup: groups.compiled, + toGroup: groups.frameworks, pbxproj: pbxproj ) compiled[path] = fileElement diff --git a/Sources/TuistGenerator/Generator/ProjectGroups.swift b/Sources/TuistGenerator/Generator/ProjectGroups.swift index 8f9c3b8e9ab..cc020268a8a 100644 --- a/Sources/TuistGenerator/Generator/ProjectGroups.swift +++ b/Sources/TuistGenerator/Generator/ProjectGroups.swift @@ -28,7 +28,7 @@ class ProjectGroups { @SortedPBXGroup var sortedMain: PBXGroup let products: PBXGroup - let compiled: PBXGroup + let frameworks: PBXGroup private let pbxproj: PBXProj private let projectGroups: [String: PBXGroup] @@ -39,21 +39,21 @@ class ProjectGroups { main: PBXGroup, projectGroups: [(name: String, group: PBXGroup)], products: PBXGroup, - compiled: PBXGroup, + frameworks: PBXGroup, pbxproj: PBXProj ) { sortedMain = main self.projectGroups = Dictionary(uniqueKeysWithValues: projectGroups) self.products = products - self.compiled = compiled + self.frameworks = frameworks self.pbxproj = pbxproj } func targetFrameworks(target: String) throws -> PBXGroup { - if let group = compiled.group(named: target) { + if let group = frameworks.group(named: target) { return group } else { - return try compiled.addGroup(named: target, options: .withoutFolder).last! + return try frameworks.addGroup(named: target, options: .withoutFolder).last! } } @@ -86,17 +86,17 @@ class ProjectGroups { let projectGroupNames = extractProjectGroupNames(from: project) let groupsToCreate = OrderedSet(projectGroupNames) var projectGroups = [(name: String, group: PBXGroup)]() - groupsToCreate.forEach { - let projectGroup = PBXGroup(children: [], sourceTree: .group, name: $0) + for item in groupsToCreate { + let projectGroup = PBXGroup(children: [], sourceTree: .group, name: item) pbxproj.add(object: projectGroup) mainGroup.children.append(projectGroup) - projectGroups.append(($0, projectGroup)) + projectGroups.append((item, projectGroup)) } - /// Compiled - let compiledGroup = PBXGroup(children: [], sourceTree: .group, name: "Compiled") - pbxproj.add(object: compiledGroup) - mainGroup.children.append(compiledGroup) + /// SDSKs & Pre-compiled frameworks + let frameworksGroup = PBXGroup(children: [], sourceTree: .group, name: "Frameworks") + pbxproj.add(object: frameworksGroup) + mainGroup.children.append(frameworksGroup) /// Products let productsGroup = PBXGroup(children: [], sourceTree: .group, name: "Products") @@ -107,7 +107,7 @@ class ProjectGroups { main: mainGroup, projectGroups: projectGroups, products: productsGroup, - compiled: compiledGroup, + frameworks: frameworksGroup, pbxproj: pbxproj ) } diff --git a/Sources/TuistGenerator/Generator/SchemeDescriptorsGenerator.swift b/Sources/TuistGenerator/Generator/SchemeDescriptorsGenerator.swift index 5778d95a44a..9e8817842c2 100644 --- a/Sources/TuistGenerator/Generator/SchemeDescriptorsGenerator.swift +++ b/Sources/TuistGenerator/Generator/SchemeDescriptorsGenerator.swift @@ -218,7 +218,7 @@ final class SchemeDescriptorsGenerator: SchemeDescriptorsGenerating { var preActions: [XCScheme.ExecutionAction] = [] var postActions: [XCScheme.ExecutionAction] = [] - try buildAction.targets.forEach { buildActionTarget in + for buildActionTarget in buildAction.targets { guard let buildActionGraphTarget = graphTraverser.target( path: buildActionTarget.projectPath, name: buildActionTarget.name @@ -230,7 +230,7 @@ final class SchemeDescriptorsGenerator: SchemeDescriptorsGenerating { generatedProjects: generatedProjects ) else { - return + continue } entries.append(XCScheme.BuildAction.Entry(buildableReference: buildableReference, buildFor: buildFor)) } @@ -294,7 +294,11 @@ final class SchemeDescriptorsGenerator: SchemeDescriptorsGenerating { ) } - try testAction.targets.forEach { testableTarget in + let skippedTests = testAction.skippedTests?.map { value in + XCScheme.TestItem(identifier: value) + } ?? [] + + for testableTarget in testAction.targets { guard let testableGraphTarget = graphTraverser.target( path: testableTarget.target.projectPath, name: testableTarget.target.name @@ -306,13 +310,14 @@ final class SchemeDescriptorsGenerator: SchemeDescriptorsGenerating { generatedProjects: generatedProjects ) else { - return + continue } let testable = XCScheme.TestableReference( skipped: testableTarget.isSkipped, parallelizable: testableTarget.isParallelizable, randomExecutionOrdering: testableTarget.isRandomExecutionOrdering, - buildableReference: reference + buildableReference: reference, + skippedTests: skippedTests ) testables.append(testable) } diff --git a/Sources/TuistGenerator/Generator/TargetGenerator.swift b/Sources/TuistGenerator/Generator/TargetGenerator.swift index 7f3d4c0be49..2d3fe4c160c 100644 --- a/Sources/TuistGenerator/Generator/TargetGenerator.swift +++ b/Sources/TuistGenerator/Generator/TargetGenerator.swift @@ -145,18 +145,17 @@ final class TargetGenerator: TargetGenerating { nativeTargets: [String: PBXNativeTarget], graphTraverser: GraphTraversing ) throws { - try targets.forEach { targetSpec in - - let dependenciesAndConditions = graphTraverser.directLocalTargetDependenciesWithConditions( + for targetSpec in targets { + let dependenciesAndConditions = graphTraverser.directLocalTargetDependencies( path: path, name: targetSpec.name - ) + ).sorted() - try dependenciesAndConditions.forEach { dependency, condition in + for dependency in dependenciesAndConditions { let nativeTarget = nativeTargets[targetSpec.name]! let nativeDependency = nativeTargets[dependency.target.name]! let pbxTargetDependency = try nativeTarget.addDependency(target: nativeDependency) - pbxTargetDependency?.applyCondition(condition, applicableTo: dependency.target) + pbxTargetDependency?.applyCondition(dependency.condition, applicableTo: targetSpec) } } } diff --git a/Sources/TuistGenerator/Generator/WorkspaceStructureGenerator.swift b/Sources/TuistGenerator/Generator/WorkspaceStructureGenerator.swift index de409e73fd0..e146d6661be 100644 --- a/Sources/TuistGenerator/Generator/WorkspaceStructureGenerator.swift +++ b/Sources/TuistGenerator/Generator/WorkspaceStructureGenerator.swift @@ -246,8 +246,8 @@ extension DirectoryStructure.Node: CustomDebugStringConvertible { return "directory: \(path.pathString) > \(graph.nodes)" case let .folderReference(path): return "folderReference: \(path.pathString)" - case let .virtualGroup(name): - return "virtualGroup: \(name)" + case let .virtualGroup(name, graph): + return "virtualGroup: \(name) > \(graph.nodes)" } } } diff --git a/Sources/TuistGenerator/GraphViz/GraphToGraphVizMapper.swift b/Sources/TuistGenerator/GraphViz/GraphToGraphVizMapper.swift index 1405e611fdb..71568c2e023 100644 --- a/Sources/TuistGenerator/GraphViz/GraphToGraphVizMapper.swift +++ b/Sources/TuistGenerator/GraphViz/GraphToGraphVizMapper.swift @@ -37,24 +37,23 @@ public final class GraphToGraphVizMapper: GraphToGraphVizMapping { let graphTraverser = GraphTraverser(graph: graph) - targetsAndDependencies.forEach { target, targetDependencies in + for (target, targetDependencies) in targetsAndDependencies { var leftNode = GraphViz.Node(target.target.name) leftNode.applyAttributes(attributes: target.styleAttributes) nodes.append(leftNode) - targetDependencies - .forEach { dependency in - var rightNode = GraphViz.Node(dependency.name) - rightNode.applyAttributes( - attributes: dependency.styleAttributes( - graphTraverser: graphTraverser - ) + for dependency in targetDependencies { + var rightNode = GraphViz.Node(dependency.name) + rightNode.applyAttributes( + attributes: dependency.styleAttributes( + graphTraverser: graphTraverser ) - nodes.append(rightNode) - let edge = GraphViz.Edge(from: leftNode, to: rightNode) - dependencies.append(edge) - } + ) + nodes.append(rightNode) + let edge = GraphViz.Edge(from: leftNode, to: rightNode) + dependencies.append(edge) + } } let sortedNodes = Set(nodes).sorted { $0.id < $1.id } @@ -81,15 +80,8 @@ extension GraphDependency { status: _ ): return path.basenameWithoutExt - case let .xcframework( - path: path, - infoPlist: _, - primaryBinaryPath: _, - linking: _, - mergeable: _, - status: _ - ): - return path.basenameWithoutExt + case let .xcframework(xcframework): + return xcframework.path.basenameWithoutExt case let .library( path: path, publicHeaders: _, @@ -109,6 +101,8 @@ extension GraphDependency { source: _ ): return String(name.split(separator: ".").first ?? "") + case let .macro(path): + return path.basename } } } diff --git a/Sources/TuistGenerator/GraphViz/NodeStyling.swift b/Sources/TuistGenerator/GraphViz/NodeStyling.swift index 3d5ac7c8666..a26ad4f2edc 100644 --- a/Sources/TuistGenerator/GraphViz/NodeStyling.swift +++ b/Sources/TuistGenerator/GraphViz/NodeStyling.swift @@ -64,6 +64,8 @@ extension GraphDependency { graphTraverser: GraphTraversing ) -> NodeStyleAttributes? { switch self { + case .macro: + return .init(fillColorName: .gray, shape: .diamond) case .sdk: return .init(fillColorName: .violet, shape: .rectangle) case .framework: diff --git a/Sources/TuistGenerator/Linter/GraphLinter.swift b/Sources/TuistGenerator/Linter/GraphLinter.swift index 54bdf2bb5e6..7b19aab3034 100644 --- a/Sources/TuistGenerator/Linter/GraphLinter.swift +++ b/Sources/TuistGenerator/Linter/GraphLinter.swift @@ -210,7 +210,7 @@ public class GraphLinter: GraphLinting { } return appClips.flatMap { appClip -> [LintingIssue] in - lint(appClip: appClip, parentApp: app) + lint(appClip: appClip.graphTarget, parentApp: app) } } @@ -233,15 +233,16 @@ public class GraphLinter: GraphLinting { .filter { $0.target.product == .watch2App } return watchApps.flatMap { watchApp -> [LintingIssue] in - let watchAppIssues = lint(watchApp: watchApp, parentApp: app) + let watchAppTarget = watchApp.graphTarget + let watchAppIssues = lint(watchApp: watchAppTarget, parentApp: app) let watchExtensions = graphTraverser.directLocalTargetDependencies( - path: watchApp.path, - name: watchApp.target.name + path: watchAppTarget.path, + name: watchAppTarget.target.name ) .filter { $0.target.product == .watch2Extension } let watchExtensionIssues = watchExtensions.flatMap { watchExtension in - lint(watchExtension: watchExtension, parentWatchApp: watchApp) + lint(watchExtension: watchExtension.graphTarget, parentWatchApp: watchAppTarget) } return watchAppIssues + watchExtensionIssues } diff --git a/Sources/TuistGenerator/Linter/SchemeLinter.swift b/Sources/TuistGenerator/Linter/SchemeLinter.swift index 11b2dca1edc..a16d2412c84 100644 --- a/Sources/TuistGenerator/Linter/SchemeLinter.swift +++ b/Sources/TuistGenerator/Linter/SchemeLinter.swift @@ -149,10 +149,10 @@ extension SchemeLinter { private func projectSchemeCantReferenceRemoteTargets(scheme: Scheme, project: Project) -> [LintingIssue] { var issues: [LintingIssue] = [] - scheme.targetDependencies().forEach { - if $0.projectPath != project.path { + for targetDependency in scheme.targetDependencies() { + if targetDependency.projectPath != project.path { issues.append(.init( - reason: "The target '\($0.name)' specified in scheme '\(scheme.name)' is not defined in the project named '\(project.name)'. Consider using a workspace scheme instead to reference a target in another project.", + reason: "The target '\(targetDependency.name)' specified in scheme '\(scheme.name)' is not defined in the project named '\(project.name)'. Consider using a workspace scheme instead to reference a target in another project.", severity: .error )) } diff --git a/Sources/TuistGenerator/Linter/SettingsLinter.swift b/Sources/TuistGenerator/Linter/SettingsLinter.swift index c6ffb3f7d66..2f1bb234094 100644 --- a/Sources/TuistGenerator/Linter/SettingsLinter.swift +++ b/Sources/TuistGenerator/Linter/SettingsLinter.swift @@ -41,7 +41,7 @@ final class SettingsLinter: SettingsLinting { } } - settings.configurations.xcconfigs().forEach { configFilePath in + for configFilePath in settings.configurations.xcconfigs() { lintPath(configFilePath) } diff --git a/Sources/TuistGenerator/Linter/StaticProductsGraphLinter.swift b/Sources/TuistGenerator/Linter/StaticProductsGraphLinter.swift index ce7dd975f34..421460d3e37 100644 --- a/Sources/TuistGenerator/Linter/StaticProductsGraphLinter.swift +++ b/Sources/TuistGenerator/Linter/StaticProductsGraphLinter.swift @@ -26,10 +26,10 @@ class StaticProductsGraphLinter: StaticProductsGraphLinting { ) -> Set { var warnings = Set() let cache = Cache() - dependencies.forEach { dependency in + for dependency in dependencies { // Skip already evaluated nodes guard cache.results(for: dependency) == nil else { - return + continue } let results = buildStaticProductsMap( visiting: dependency, @@ -158,8 +158,8 @@ class StaticProductsGraphLinter: StaticProductsGraphLinting { private func isStaticProduct(_ dependency: GraphDependency, graphTraverser: GraphTraversing) -> Bool { switch dependency { - case let .xcframework(_, _, _, linking, _, _): - return linking == .static + case let .xcframework(xcframework): + return xcframework.linking == .static case let .framework(_, _, _, _, linking, _, _, _): return linking == .static case let .library(_, _, linking, _, _): @@ -176,7 +176,7 @@ class StaticProductsGraphLinter: StaticProductsGraphLinting { case let .target(name, path): guard let target = graphTraverser.target(path: path, name: name) else { return false } return target.target.product.isStatic - case .sdk: + case .sdk, .macro: return false } } @@ -228,6 +228,10 @@ class StaticProductsGraphLinter: StaticProductsGraphLinting { // System Extension can safely link the same static products as apps // as they are an independent product return false + case (_, .macro): + // Macro executables can safely link the same static products as the targets + // depending on the executable's parent target (e.g. framework or library). + return false default: return true } diff --git a/Sources/TuistGenerator/Linter/TargetLinter.swift b/Sources/TuistGenerator/Linter/TargetLinter.swift index 67fc7979b8c..f1011b96415 100644 --- a/Sources/TuistGenerator/Linter/TargetLinter.swift +++ b/Sources/TuistGenerator/Linter/TargetLinter.swift @@ -40,7 +40,7 @@ class TargetLinter: TargetLinting { issues.append(contentsOf: validateCoreDataModelsExist(target: target)) issues.append(contentsOf: validateCoreDataModelVersionsExist(target: target)) issues.append(contentsOf: lintMergeableLibrariesOnlyAppliesToDynamicTargets(target: target)) - target.scripts.forEach { script in + for script in target.scripts { issues.append(contentsOf: targetScriptLinter.lint(script)) } return issues diff --git a/Sources/TuistGenerator/Log/Logger.swift b/Sources/TuistGenerator/Log/Logger.swift index ebcedf1de61..27afc88c525 100644 --- a/Sources/TuistGenerator/Log/Logger.swift +++ b/Sources/TuistGenerator/Log/Logger.swift @@ -1,2 +1,3 @@ import TuistSupport + let logger = Logger(label: "io.tuist.generator") diff --git a/Sources/TuistGenerator/Mappers/AutogeneratedSchemesProjectMapper.swift b/Sources/TuistGenerator/Mappers/AutogeneratedSchemesProjectMapper.swift index b0a08813d81..d724a62b39f 100644 --- a/Sources/TuistGenerator/Mappers/AutogeneratedSchemesProjectMapper.swift +++ b/Sources/TuistGenerator/Mappers/AutogeneratedSchemesProjectMapper.swift @@ -20,7 +20,7 @@ public final class AutogeneratedSchemesProjectMapper: ProjectMapping { // swiftl var buildTargets: Set = [] var testTargets: Set = [] var runTargets: Set = [] - project.targets.forEach { target in + for target in project.targets { switch target.product { case .app, .appClip, .commandLineTool, .watch2App, .xpc: runTargets.insert(target) diff --git a/Sources/TuistGenerator/Mappers/ExplicitDependencyGraphMapper.swift b/Sources/TuistGenerator/Mappers/ExplicitDependencyGraphMapper.swift new file mode 100644 index 00000000000..0b5d1d4eaf4 --- /dev/null +++ b/Sources/TuistGenerator/Mappers/ExplicitDependencyGraphMapper.swift @@ -0,0 +1,195 @@ +import Foundation +import TSCBasic +import TuistCore +import TuistGraph +import TuistSupport + +/// A target mapper that enforces explicit dependneices by adding custom build directories +public struct ExplicitDependencyGraphMapper: GraphMapping { + public init() {} + + public func map(graph: Graph) async throws -> (Graph, [SideEffectDescriptor]) { + if !graph.packages.isEmpty { + return ( + graph, + [] + ) + } + + let graphTraverser = GraphTraverser(graph: graph) + + var graph = graph + + graph.projects = Dictionary(uniqueKeysWithValues: graph.projects.map { projectPath, project in + var project = project + project.targets = project.targets.map { target in + let graphTarget = GraphTarget(path: projectPath, target: target, project: project) + let projectDebugConfigurations = project.settings.configurations.keys + .filter { $0.variant == .debug } + .map(\.name) + + let mappedTarget = map( + graphTarget, + graphTraverser: graphTraverser, + debugConfigurations: projectDebugConfigurations + .isEmpty ? [project.defaultDebugBuildConfigurationName] : projectDebugConfigurations + ) + + return mappedTarget + } + + return (projectPath, project) + }) + return (graph, []) + } + + private func map(_ graphTarget: GraphTarget, graphTraverser: GraphTraversing, debugConfigurations: [String]) -> Target { + let allTargetDependencies = graphTraverser.allTargetDependencies( + path: graphTarget.path, + name: graphTarget.target.name + ) + let frameworkSearchPaths = allTargetDependencies + .map(\.target.productName).map { + "$(CONFIGURATION_BUILD_DIR)$(TARGET_BUILD_SUBPATH)/\($0)" + } + + switch graphTarget.target.product { + case .dynamicLibrary, .staticLibrary, .framework, .staticFramework, .bundle: + break + default: + return graphTarget.target + } + + var additionalSettings: SettingsDictionary = [ + "TARGET_BUILD_DIR": "$(CONFIGURATION_BUILD_DIR)$(TARGET_BUILD_SUBPATH)/$(PRODUCT_NAME)", + "BUILT_PRODUCTS_DIR": "$(CONFIGURATION_BUILD_DIR)$(TARGET_BUILD_SUBPATH)/$(PRODUCT_NAME)", + ] + + if graphTarget.project.isExternal { + additionalSettings["FRAMEWORK_SEARCH_PATHS"] = .array(["$(CONFIGURATION_BUILD_DIR)$(TARGET_BUILD_SUBPATH)"]) + } else if !frameworkSearchPaths.isEmpty { + additionalSettings["FRAMEWORK_SEARCH_PATHS"] = .array(frameworkSearchPaths) + } + + var target = graphTarget.target + target.settings = Settings( + base: target.settings?.base ?? [:], + baseDebug: additionalSettings, + configurations: [:] + ) + + let copyBuiltProductsScript: String + let builtProductsScriptInputPaths: [String] + let builtProductsScriptOutputPaths: [String] + + switch target.product { + case .staticLibrary: + let (libScript, libInputPaths, libOutputPaths) = copyBuiltProductsToSharedDirectory( + debugConfigurations: debugConfigurations, + extensionName: "a", + prefix: "lib" + ) + let (moduleScript, moduleInputPaths, moduleOutputPaths) = copyBuiltProductsToSharedDirectory( + debugConfigurations: debugConfigurations, + extensionName: "swiftmodule" + ) + copyBuiltProductsScript = [libScript, moduleScript].joined(separator: "\n") + + builtProductsScriptInputPaths = libInputPaths + moduleInputPaths + builtProductsScriptOutputPaths = libOutputPaths + moduleOutputPaths + case .dynamicLibrary: + ( + copyBuiltProductsScript, + builtProductsScriptInputPaths, + builtProductsScriptOutputPaths + ) = copyBuiltProductsToSharedDirectory( + debugConfigurations: debugConfigurations, + extensionName: "swiftmodule" + ) + case .bundle: + ( + copyBuiltProductsScript, + builtProductsScriptInputPaths, + builtProductsScriptOutputPaths + ) = copyBuiltProductsToSharedDirectory( + debugConfigurations: debugConfigurations, + extensionName: "bundle" + ) + case .framework, .staticFramework: + ( + copyBuiltProductsScript, + builtProductsScriptInputPaths, + builtProductsScriptOutputPaths + ) = copyBuiltProductsToSharedDirectory( + debugConfigurations: debugConfigurations, + extensionName: "framework" + ) + default: + return graphTarget.target + } + + target = target.with( + scripts: target.scripts + [ + TargetScript( + name: "Copy Built Products for Explicit Dependencies", + order: .post, + script: .embedded( + """ + # This script copies built products into the shared directory to be available for app and other targets that don't have scoped directories + # If you try to archive any of the configurations seen in the output paths, the operation will fail due to `Multiple commands produce` error + + \(copyBuiltProductsScript) + """ + ), + inputPaths: builtProductsScriptInputPaths, + outputPaths: builtProductsScriptOutputPaths + ), + ] + ) + + return target + } + + private func copyBuiltProductsToSharedDirectory( + debugConfigurations: [String], + extensionName: String, + prefix: String = "" + ) -> (String, [String], [String]) { + let script = debugConfigurations.map { + copyScript(for: $0, extensionName: extensionName, prefix: prefix) + } + .joined(separator: "\n") + + return ( + script, + debugConfigurations.map { + "$(BUILD_DIR)/\($0)$(EFFECTIVE_PLATFORM_NAME)$(TARGET_BUILD_SUBPATH)/$(PRODUCT_NAME)/\(prefix)$(PRODUCT_NAME).\(extensionName)" + }, + debugConfigurations.map { + "$(BUILD_DIR)/\($0)$(EFFECTIVE_PLATFORM_NAME)$(TARGET_BUILD_SUBPATH)/\(prefix)$(PRODUCT_NAME).\(extensionName)" + } + ) + } + + private func copyScript( + for configuration: String, + extensionName: String, + prefix: String + ) -> String { + """ + FILE="$BUILD_DIR/\(configuration)$EFFECTIVE_PLATFORM_NAME$TARGET_BUILD_SUBPATH/$PRODUCT_NAME/\(prefix)$PRODUCT_NAME.\( + extensionName + )" + DESTINATION_FILE="$BUILD_DIR/\(configuration)$EFFECTIVE_PLATFORM_NAME$TARGET_BUILD_SUBPATH/\(prefix)$PRODUCT_NAME.\( + extensionName + )" + if [[ -d "$FILE" && ! -d "$DESTINATION_FILE" ]]; then + ln -s "$FILE" "$DESTINATION_FILE" + fi + + if [[ -f "$FILE" && ! -f "$DESTINATION_FILE" ]]; then + ln -s "$FILE" "$DESTINATION_FILE" + fi + """ + } +} diff --git a/Sources/TuistGenerator/Mappers/ModuleMapMapper.swift b/Sources/TuistGenerator/Mappers/ModuleMapMapper.swift index 6c7b31a04f8..bec658bbc95 100644 --- a/Sources/TuistGenerator/Mappers/ModuleMapMapper.swift +++ b/Sources/TuistGenerator/Mappers/ModuleMapMapper.swift @@ -62,8 +62,8 @@ public final class ModuleMapMapper: WorkspaceMapping { public func map(workspace: WorkspaceWithProjects) throws -> (WorkspaceWithProjects, [SideEffectDescriptor]) { let (projectsByPath, targetsByName) = Self.makeProjectsByPathWithTargetsByName(workspace: workspace) var targetToModuleMaps: [TargetID: Set] = [:] - try workspace.projects.forEach { project in - try project.targets.forEach { target in + for project in workspace.projects { + for target in project.targets { try Self.dependenciesModuleMaps( workspace: workspace, project: project, @@ -123,9 +123,9 @@ public final class ModuleMapMapper: WorkspaceMapping { { var projectsByPath = [AbsolutePath: Project]() var targetsByName = [String: Target]() - workspace.projects.forEach { project in + for project in workspace.projects { projectsByPath[project.path] = project - project.targets.forEach { target in + for target in project.targets { targetsByName[target.name] = target } } diff --git a/Sources/TuistGenerator/Mappers/ResourcesProjectMapper.swift b/Sources/TuistGenerator/Mappers/ResourcesProjectMapper.swift index f157a730c7d..8c4d5370bb9 100644 --- a/Sources/TuistGenerator/Mappers/ResourcesProjectMapper.swift +++ b/Sources/TuistGenerator/Mappers/ResourcesProjectMapper.swift @@ -18,7 +18,7 @@ public class ResourcesProjectMapper: ProjectMapping { var sideEffects: [SideEffectDescriptor] = [] var targets: [Target] = [] - try project.targets.forEach { target in + for target in project.targets { let (mappedTargets, targetSideEffects) = try mapTarget(target, project: project) targets.append(contentsOf: mappedTargets) sideEffects.append(contentsOf: targetSideEffects) diff --git a/Sources/TuistGenerator/Mappers/SynthesizedResourceInterfaceProjectMapper.swift b/Sources/TuistGenerator/Mappers/SynthesizedResourceInterfaceProjectMapper.swift index bc8bfd01bf0..aad58ab2f99 100644 --- a/Sources/TuistGenerator/Mappers/SynthesizedResourceInterfaceProjectMapper.swift +++ b/Sources/TuistGenerator/Mappers/SynthesizedResourceInterfaceProjectMapper.swift @@ -161,23 +161,23 @@ public final class SynthesizedResourceInterfaceProjectMapper: ProjectMapping { / switch resourceSynthesizer.parser { case .strings: // This file kind is localizable, let's order files based on it - var regionPriorityQueue: [String] = [] - - if let developmentRegion { - regionPriorityQueue.insert(developmentRegion, at: 0) - } - - // Let's sort paths moving the development region localization's one at first - let prioritizedPaths = paths.filter { path in - regionPriorityQueue.map { path.parentDirectory.basename.contains($0) }.contains(true) - } - - let unprioritizedPaths = paths.filter { path in - !regionPriorityQueue.map { path.parentDirectory.basename.contains($0) }.contains(true) - } - - paths = prioritizedPaths + unprioritizedPaths - + paths = { + guard let developmentRegion else { return paths } + return paths.sorted { lhs, rhs in + let lhsBasename = lhs.parentDirectory.basenameWithoutExt + let rhsBasename = rhs.parentDirectory.basenameWithoutExt + + if lhsBasename == developmentRegion { + return true + } else if rhsBasename == developmentRegion { + return false + } else if lhsBasename.contains(developmentRegion), rhsBasename.contains(developmentRegion) { + return lhsBasename < rhsBasename + } else { + return lhsBasename < rhsBasename + } + } + }() case .assets, .coreData, .fonts, .interfaceBuilder, .json, .plists, .yaml, .files: break } diff --git a/Sources/TuistGenerator/Utils/SettingsHelper.swift b/Sources/TuistGenerator/Utils/SettingsHelper.swift index 8cf63281bb8..c051d12aa9a 100644 --- a/Sources/TuistGenerator/Utils/SettingsHelper.swift +++ b/Sources/TuistGenerator/Utils/SettingsHelper.swift @@ -8,7 +8,7 @@ final class SettingsHelper { buildSettings: inout SettingsDictionary, with other: SettingsDictionary ) { - other.forEach { key, newValue in + for (key, newValue) in other { buildSettings[key] = merge(oldValue: buildSettings[key], newValue: newValue).normalize() } } diff --git a/Sources/TuistGraph/Graph/ConditionalGraphTarget.swift b/Sources/TuistGraph/Graph/ConditionalGraphTarget.swift new file mode 100644 index 00000000000..b4ee787e2f8 --- /dev/null +++ b/Sources/TuistGraph/Graph/ConditionalGraphTarget.swift @@ -0,0 +1,49 @@ +import Foundation +import TSCBasic + +public struct GraphTargetReference: Equatable, Comparable, Hashable, CustomDebugStringConvertible, CustomStringConvertible, + Codable +{ + /// Path to the directory that contains the project where the target is defined. + public let graphTarget: GraphTarget + + public var target: Target { graphTarget.target } + + /// Platforms the target is conditionally deployed to. + public let condition: PlatformCondition? + + public init(target: GraphTarget, condition: PlatformCondition? = nil) { + graphTarget = target + self.condition = condition + } + + public static func < (lhs: GraphTargetReference, rhs: GraphTargetReference) -> Bool { + lhs.graphTarget < rhs.graphTarget +// guard let +// return (lhs.condition, lhs.graphTarget) < (rhs.condtion, rhs.graphTarget) +// +// +// switch (lhs.condition, rhs.condition) { +// case (let lhsCondtion, let rhsCondtion): +// return (lhsCondition, lhs.graphTarget) < (rhsCondtion, rhs.graphTarget) +// case +// } + } + + // MARK: - CustomDebugStringConvertible/CustomStringConvertible + + public var debugDescription: String { + description + } + + public var description: String { + "Target '\(target.name)' at path '\(graphTarget.project.path)'" + } +} + +// +// extension GraphTarget { +// public func reference(_ condition: PlatformCondition?) -> GraphTargetReference { +// return GraphTargetReference(target: self, condition: condition) +// } +// } diff --git a/Sources/TuistGraph/Graph/Graph.swift b/Sources/TuistGraph/Graph/Graph.swift index 5b5467127ee..6799d08f636 100644 --- a/Sources/TuistGraph/Graph/Graph.swift +++ b/Sources/TuistGraph/Graph/Graph.swift @@ -1,28 +1,6 @@ import Foundation import TSCBasic -/// A directed edge linking representing a dependent relationship -/// e.g. `from` (MainApp) depends on `to` (UIKit) -public struct GraphEdge: Hashable, Codable { - public let from: GraphDependency - public let to: GraphDependency - public init(from: GraphDependency, to: GraphDependency) { - self.from = from - self.to = to - } -} - -extension [GraphEdge: PlatformCondition] { - public subscript(_ edge: (GraphDependency, GraphDependency)) -> PlatformCondition? { - get { - self[GraphEdge(from: edge.0, to: edge.1)] - } - set { - self[GraphEdge(from: edge.0, to: edge.1)] = newValue - } - } -} - /// A directed acyclic graph (DAG) that Tuist uses to represent the dependency tree. public struct Graph: Equatable, Codable { /// The name of the graph @@ -72,3 +50,24 @@ public struct Graph: Equatable, Codable { self.dependencyConditions = dependencyConditions } } + +/// Convenience accessors to work with `GraphTarget` and `GraphDependency` types while traversing the graph +extension [GraphEdge: PlatformCondition] { + public subscript(_ edge: (GraphDependency, GraphDependency)) -> PlatformCondition? { + get { + self[GraphEdge(from: edge.0, to: edge.1)] + } + set { + self[GraphEdge(from: edge.0, to: edge.1)] = newValue + } + } + + public subscript(_ edge: (GraphDependency, GraphTarget)) -> PlatformCondition? { + get { + self[GraphEdge(from: edge.0, to: edge.1)] + } + set { + self[GraphEdge(from: edge.0, to: edge.1)] = newValue + } + } +} diff --git a/Sources/TuistGraph/Graph/GraphDependency.swift b/Sources/TuistGraph/Graph/GraphDependency.swift index c10016ea739..cc5dd70e7db 100644 --- a/Sources/TuistGraph/Graph/GraphDependency.swift +++ b/Sources/TuistGraph/Graph/GraphDependency.swift @@ -2,6 +2,40 @@ import Foundation import TSCBasic public enum GraphDependency: Hashable, CustomStringConvertible, Comparable, Codable { + public struct XCFramework: Hashable, CustomStringConvertible, Comparable, Codable { + public let path: AbsolutePath + public let infoPlist: XCFrameworkInfoPlist + public let primaryBinaryPath: AbsolutePath + public let linking: BinaryLinking + public let mergeable: Bool + public let status: FrameworkStatus + + public init( + path: AbsolutePath, + infoPlist: XCFrameworkInfoPlist, + primaryBinaryPath: AbsolutePath, + linking: BinaryLinking, + mergeable: Bool, + status: FrameworkStatus, + macroPath _: AbsolutePath? + ) { + self.path = path + self.infoPlist = infoPlist + self.primaryBinaryPath = primaryBinaryPath + self.linking = linking + self.mergeable = mergeable + self.status = status + } + + public var description: String { + "xcframework '\(path.basename)'" + } + + public static func < (lhs: GraphDependency.XCFramework, rhs: GraphDependency.XCFramework) -> Bool { + lhs.description < rhs.description + } + } + public enum PackageProductType: String, Hashable, CustomStringConvertible, Comparable, Codable { public var description: String { rawValue @@ -16,15 +50,7 @@ public enum GraphDependency: Hashable, CustomStringConvertible, Comparable, Coda } } - /// A dependency that represents a pre-compiled .xcframework. - case xcframework( - path: AbsolutePath, - infoPlist: XCFrameworkInfoPlist, - primaryBinaryPath: AbsolutePath, - linking: BinaryLinking, - mergeable: Bool, - status: FrameworkStatus - ) + case xcframework(GraphDependency.XCFramework) /// A dependency that represents a pre-compiled framework. case framework( @@ -47,6 +73,9 @@ public enum GraphDependency: Hashable, CustomStringConvertible, Comparable, Coda swiftModuleMap: AbsolutePath? ) + /// A macOS executable that represents a macro + case macro(path: AbsolutePath) + /// A dependency that represents a pre-compiled bundle. case bundle(path: AbsolutePath) @@ -61,9 +90,10 @@ public enum GraphDependency: Hashable, CustomStringConvertible, Comparable, Coda public func hash(into hasher: inout Hasher) { switch self { - case let .xcframework(path, _, _, _, _, _): - hasher.combine("xcframework") + case let .macro(path): hasher.combine(path) + case let .xcframework(xcframework): + hasher.combine(xcframework) case let .framework(path, _, _, _, _, _, _, _): hasher.combine("framework") hasher.combine(path) @@ -93,6 +123,7 @@ public enum GraphDependency: Hashable, CustomStringConvertible, Comparable, Coda public var isTarget: Bool { switch self { + case .macro: return false case .xcframework: return false case .framework: return false case .library: return false @@ -108,8 +139,10 @@ public enum GraphDependency: Hashable, CustomStringConvertible, Comparable, Coda */ public var isStaticPrecompiled: Bool { switch self { - case let .xcframework(_, _, _, linking, _, _), - let .framework(_, _, _, _, linking, _, _, _), + case .macro: return false + case let .xcframework(xcframework): + return xcframework.linking == .static + case let .framework(_, _, _, _, linking, _, _, _), let .library(_, _, linking, _, _): return linking == .static case .bundle: return false case .packageProduct: return false @@ -123,8 +156,10 @@ public enum GraphDependency: Hashable, CustomStringConvertible, Comparable, Coda */ public var isDynamicPrecompiled: Bool { switch self { - case let .xcframework(_, _, _, linking, _, _), - let .framework(_, _, _, _, linking, _, _, _), + case .macro: return false + case let .xcframework(xcframework): + return xcframework.linking == .dynamic + case let .framework(_, _, _, _, linking, _, _, _), let .library(_, _, linking, _, _): return linking == .dynamic case .bundle: return false case .packageProduct: return false @@ -135,6 +170,7 @@ public enum GraphDependency: Hashable, CustomStringConvertible, Comparable, Coda public var isPrecompiled: Bool { switch self { + case .macro: return true case .xcframework: return true case .framework: return true case .library: return true @@ -145,6 +181,47 @@ public enum GraphDependency: Hashable, CustomStringConvertible, Comparable, Coda } } + public var isLinkable: Bool { + switch self { + case .macro: return false + case .xcframework: return true + case .framework: return true + case .library: return true + case .bundle: return false + case .packageProduct: return true + case .target: return true + case .sdk: return true + } + } + + public var isPrecompiledMacro: Bool { + switch self { + case .macro: return true + case .xcframework: return false + case .framework: return false + case .library: return false + case .bundle: return false + case .packageProduct: return false + case .target: return false + case .sdk: return false + } + } + + public var isPrecompiledDynamicAndLinkable: Bool { + switch self { + case .macro: return false + case let .xcframework(xcframework): + return xcframework.linking == .dynamic + case let .framework(_, _, _, _, linking, _, _, _), + let .library(path: _, publicHeaders: _, linking: linking, architectures: _, swiftModuleMap: _): + return linking == .dynamic + case .bundle: return false + case .packageProduct: return false + case .target: return false + case .sdk: return false + } + } + // MARK: - Internal public var targetDependency: (name: String, path: AbsolutePath)? { @@ -160,6 +237,8 @@ public enum GraphDependency: Hashable, CustomStringConvertible, Comparable, Coda public var description: String { switch self { + case .macro: + return "macro '\(name)'" case .xcframework: return "xcframework '\(name)'" case .framework: @@ -179,8 +258,10 @@ public enum GraphDependency: Hashable, CustomStringConvertible, Comparable, Coda public var name: String { switch self { - case let .xcframework(path, _, _, _, _, _): + case let .macro(path): return path.basename + case let .xcframework(xcframework): + return xcframework.path.basename case let .framework(path, _, _, _, _, _, _, _): return path.basename case let .library(path, _, _, _, _): diff --git a/Sources/TuistGraph/Graph/GraphEdge.swift b/Sources/TuistGraph/Graph/GraphEdge.swift new file mode 100644 index 00000000000..da6a397561e --- /dev/null +++ b/Sources/TuistGraph/Graph/GraphEdge.swift @@ -0,0 +1,17 @@ +import Foundation + +/// A directed edge linking representing a dependent relationship +/// e.g. `from` (MainApp) depends on `to` (UIKit) +public struct GraphEdge: Hashable, Codable { + public let from: GraphDependency + public let to: GraphDependency + public init(from: GraphDependency, to: GraphDependency) { + self.from = from + self.to = to + } + + public init(from: GraphDependency, to: GraphTarget) { + self.from = from + self.to = .target(name: to.target.name, path: to.path) + } +} diff --git a/Sources/TuistGraph/Models/BinaryArchitecture.swift b/Sources/TuistGraph/Models/BinaryArchitecture.swift index 1e18cecb90e..ce2321a31fa 100644 --- a/Sources/TuistGraph/Models/BinaryArchitecture.swift +++ b/Sources/TuistGraph/Models/BinaryArchitecture.swift @@ -11,7 +11,7 @@ public enum BinaryArchitecture: String, Codable { case arm64e } -public enum BinaryLinking: String, Codable { +public enum BinaryLinking: String, Hashable, Codable { case `static`, dynamic } diff --git a/Sources/TuistGraph/Models/ConfigGenerationOptions.swift b/Sources/TuistGraph/Models/ConfigGenerationOptions.swift index 9fe3ec6149b..c2150f6f589 100644 --- a/Sources/TuistGraph/Models/ConfigGenerationOptions.swift +++ b/Sources/TuistGraph/Models/ConfigGenerationOptions.swift @@ -13,17 +13,20 @@ extension Config { public let disablePackageVersionLocking: Bool public let clonedSourcePackagesDirPath: AbsolutePath? public let staticSideEffectsWarningTargets: StaticSideEffectsWarningTargets + public let enforceExplicitDependencies: Bool public init( resolveDependenciesWithSystemScm: Bool, disablePackageVersionLocking: Bool, clonedSourcePackagesDirPath: AbsolutePath? = nil, - staticSideEffectsWarningTargets: StaticSideEffectsWarningTargets = .all + staticSideEffectsWarningTargets: StaticSideEffectsWarningTargets = .all, + enforceExplicitDependencies: Bool = false ) { self.resolveDependenciesWithSystemScm = resolveDependenciesWithSystemScm self.disablePackageVersionLocking = disablePackageVersionLocking self.clonedSourcePackagesDirPath = clonedSourcePackagesDirPath self.staticSideEffectsWarningTargets = staticSideEffectsWarningTargets + self.enforceExplicitDependencies = enforceExplicitDependencies } } } diff --git a/Sources/TuistGraph/Models/Dependencies/PackageSettings.swift b/Sources/TuistGraph/Models/Dependencies/PackageSettings.swift new file mode 100644 index 00000000000..8c0b5479077 --- /dev/null +++ b/Sources/TuistGraph/Models/Dependencies/PackageSettings.swift @@ -0,0 +1,39 @@ +import Foundation + +/// Contains the description of custom SPM settings +public struct PackageSettings: Equatable { + /// The custom `Product` types to be used for SPM targets. + public let productTypes: [String: Product] + + // The base settings to be used for targets generated from SwiftPackageManager + public let baseSettings: Settings + + /// The custom `Settings` to be applied to SPM targets + public let targetSettings: [String: SettingsDictionary] + + /// The custom project options for each project generated from a swift package + public let projectOptions: [String: TuistGraph.Project.Options] + + /// The custom set of `platforms` that are used by your project + public let platforms: Set + + /// Initializes a new `PackageSettings` instance. + /// - Parameters: + /// - productTypes: The custom `Product` types to be used for SPM targets. + /// - baseSettings: The base settings to be used for targets generated from SwiftPackageManager + /// - targetSettings: The custom `SettingsDictionary` to be applied to denoted targets + /// - projectOptions: The custom project options for each project generated from a swift package + public init( + productTypes: [String: Product], + baseSettings: Settings, + targetSettings: [String: SettingsDictionary], + projectOptions: [String: TuistGraph.Project.Options] = [:], + platforms: Set + ) { + self.productTypes = productTypes + self.baseSettings = baseSettings + self.targetSettings = targetSettings + self.projectOptions = projectOptions + self.platforms = platforms + } +} diff --git a/Sources/TuistGraph/Models/Metadata/XCFrameworkMetadata.swift b/Sources/TuistGraph/Models/Metadata/XCFrameworkMetadata.swift index 8f7a8eeae64..21e78ffd11d 100644 --- a/Sources/TuistGraph/Models/Metadata/XCFrameworkMetadata.swift +++ b/Sources/TuistGraph/Models/Metadata/XCFrameworkMetadata.swift @@ -9,6 +9,7 @@ public struct XCFrameworkMetadata: Equatable { public var linking: BinaryLinking public var mergeable: Bool public var status: FrameworkStatus + public var macroPath: AbsolutePath? public init( path: AbsolutePath, @@ -16,7 +17,8 @@ public struct XCFrameworkMetadata: Equatable { primaryBinaryPath: AbsolutePath, linking: BinaryLinking, mergeable: Bool, - status: FrameworkStatus + status: FrameworkStatus, + macroPath: AbsolutePath? ) { self.path = path self.infoPlist = infoPlist @@ -24,5 +26,6 @@ public struct XCFrameworkMetadata: Equatable { self.linking = linking self.mergeable = mergeable self.status = status + self.macroPath = macroPath } } diff --git a/Sources/TuistGraph/Models/PlatformCondition.swift b/Sources/TuistGraph/Models/PlatformCondition.swift index 348e847abcc..1833efeb18a 100644 --- a/Sources/TuistGraph/Models/PlatformCondition.swift +++ b/Sources/TuistGraph/Models/PlatformCondition.swift @@ -6,6 +6,11 @@ public struct PlatformCondition: Codable, Hashable, Equatable, Comparable { lhs.platformFilters < rhs.platformFilters } + public static func < (lhs: PlatformCondition, rhs: PlatformCondition?) -> Bool { + guard let rhsFilters = rhs?.platformFilters else { return false } + return lhs.platformFilters < rhsFilters + } + public let platformFilters: PlatformFilters private init(platformFilters: PlatformFilters) { self.platformFilters = platformFilters diff --git a/Sources/TuistGraph/Models/PlatformFilter.swift b/Sources/TuistGraph/Models/PlatformFilter.swift index 801a0269d74..00f2b2ed29e 100644 --- a/Sources/TuistGraph/Models/PlatformFilter.swift +++ b/Sources/TuistGraph/Models/PlatformFilter.swift @@ -39,6 +39,25 @@ public enum PlatformFilter: Comparable, Hashable, Codable, CaseIterable { return "xros" } } + + /// It returns the platform that the filter is filtering for + public var platform: Platform? { + switch self { + case .ios, .catalyst: + return .iOS + case .macos: + return .macOS + case .tvos: + return .tvOS + case .watchos: + return .watchOS + case .visionos: + return .visionOS + case .driverkit: + // TODO: Add support for it + return nil + } + } } extension PlatformFilters { diff --git a/Sources/TuistGraph/Models/Settings.swift b/Sources/TuistGraph/Models/Settings.swift index 04d826ee648..5684ce288ad 100644 --- a/Sources/TuistGraph/Models/Settings.swift +++ b/Sources/TuistGraph/Models/Settings.swift @@ -39,7 +39,7 @@ extension SettingsDictionary { with other: SettingsDictionary, for platform: Platform ) { - other.forEach { key, newValue in + for (key, newValue) in other { if self[key] == nil { self[key] = newValue } else if self[key] != newValue { @@ -104,6 +104,8 @@ public struct Settings: Equatable, Codable { // MARK: - Attributes public let base: SettingsDictionary + /// Base settings applied only for configurations of `variant == .debug` + public let baseDebug: SettingsDictionary public let configurations: [BuildConfiguration: Configuration?] public let defaultSettings: DefaultSettings @@ -111,10 +113,12 @@ public struct Settings: Equatable, Codable { public init( base: SettingsDictionary = [:], + baseDebug: SettingsDictionary = [:], configurations: [BuildConfiguration: Configuration?], defaultSettings: DefaultSettings = .recommended ) { self.base = base + self.baseDebug = baseDebug self.configurations = configurations self.defaultSettings = defaultSettings } diff --git a/Sources/TuistGraph/Models/Target.swift b/Sources/TuistGraph/Models/Target.swift index 06fe0629743..8589b391c84 100644 --- a/Sources/TuistGraph/Models/Target.swift +++ b/Sources/TuistGraph/Models/Target.swift @@ -252,13 +252,13 @@ public struct Target: Equatable, Hashable, Comparable, Codable { /// Determines if the target is able to embed a watch application /// i.e. a product that can be bundled with a watchOS application public func canEmbedWatchApplications() -> Bool { - isExclusiveTo(.iOS) && product == .app + supports(.iOS) && product == .app } /// Determines if the target is able to embed an system extension /// i.e. a product that can be bundled with a macOS application public func canEmbedSystemExtensions() -> Bool { - isExclusiveTo(.macOS) && product == .app + supports(.macOS) && product == .app } /// Return the a set of PlatformFilters to control linking based on what platform is being compiled diff --git a/Sources/TuistGraph/Models/TargetDependency.swift b/Sources/TuistGraph/Models/TargetDependency.swift index 7ab4b8d6849..bfee06a3669 100644 --- a/Sources/TuistGraph/Models/TargetDependency.swift +++ b/Sources/TuistGraph/Models/TargetDependency.swift @@ -1,7 +1,7 @@ import Foundation import TSCBasic -public enum FrameworkStatus: String, Codable { +public enum FrameworkStatus: String, Hashable, Codable { case required case optional } diff --git a/Sources/TuistGraph/Models/TargetScript.swift b/Sources/TuistGraph/Models/TargetScript.swift index 93ec8365ee1..a32b82fb043 100644 --- a/Sources/TuistGraph/Models/TargetScript.swift +++ b/Sources/TuistGraph/Models/TargetScript.swift @@ -71,13 +71,13 @@ public struct TargetScript: Equatable, Codable { } /// List of input file paths - public let inputPaths: [AbsolutePath] + public let inputPaths: [String] /// List of input filelist paths public let inputFileListPaths: [AbsolutePath] /// List of output file paths - public let outputPaths: [AbsolutePath] + public let outputPaths: [String] /// List of output filelist paths public let outputFileListPaths: [AbsolutePath] @@ -116,9 +116,9 @@ public struct TargetScript: Equatable, Codable { name: String, order: Order, script: Script = .embedded(""), - inputPaths: [AbsolutePath] = [], + inputPaths: [String] = [], inputFileListPaths: [AbsolutePath] = [], - outputPaths: [AbsolutePath] = [], + outputPaths: [String] = [], outputFileListPaths: [AbsolutePath] = [], showEnvVarsInLog: Bool = true, basedOnDependencyAnalysis: Bool? = nil, diff --git a/Sources/TuistGraph/Models/TestAction.swift b/Sources/TuistGraph/Models/TestAction.swift index bdecf66d839..062e3e915af 100644 --- a/Sources/TuistGraph/Models/TestAction.swift +++ b/Sources/TuistGraph/Models/TestAction.swift @@ -18,6 +18,7 @@ public struct TestAction: Equatable, Codable { public var language: String? public var region: String? public var preferredScreenCaptureFormat: ScreenCaptureFormat? + public var skippedTests: [String]? // MARK: - Init @@ -35,7 +36,8 @@ public struct TestAction: Equatable, Codable { language: String? = nil, region: String? = nil, preferredScreenCaptureFormat: ScreenCaptureFormat? = nil, - testPlans: [TestPlan]? = nil + testPlans: [TestPlan]? = nil, + skippedTests: [String]? = nil ) { self.testPlans = testPlans self.targets = targets @@ -51,5 +53,6 @@ public struct TestAction: Equatable, Codable { self.language = language self.region = region self.preferredScreenCaptureFormat = preferredScreenCaptureFormat + self.skippedTests = skippedTests } } diff --git a/Sources/TuistGraph/Models/XCFrameworkInfoPlist.swift b/Sources/TuistGraph/Models/XCFrameworkInfoPlist.swift index e306f803392..e7b7986f208 100644 --- a/Sources/TuistGraph/Models/XCFrameworkInfoPlist.swift +++ b/Sources/TuistGraph/Models/XCFrameworkInfoPlist.swift @@ -2,13 +2,13 @@ import Foundation import TSCBasic /// It represents th Info.plist contained in an .xcframework bundle. -public struct XCFrameworkInfoPlist: Codable, Equatable { +public struct XCFrameworkInfoPlist: Codable, Hashable, Equatable { private enum CodingKeys: String, CodingKey { case libraries = "AvailableLibraries" } /// It represents a library inside an .xcframework - public struct Library: Codable, Equatable { + public struct Library: Codable, Hashable, Equatable { private enum CodingKeys: String, CodingKey { case identifier = "LibraryIdentifier" case path = "LibraryPath" @@ -17,7 +17,7 @@ public struct XCFrameworkInfoPlist: Codable, Equatable { } /// It represents the library's platform. - public enum Platform: String, Codable { + public enum Platform: String, Hashable, Codable { case ios } diff --git a/Sources/TuistGraphTesting/Graph/GraphDependency+TestData.swift b/Sources/TuistGraphTesting/Graph/GraphDependency+TestData.swift index be8c7df009e..7d190bf3fea 100644 --- a/Sources/TuistGraphTesting/Graph/GraphDependency+TestData.swift +++ b/Sources/TuistGraphTesting/Graph/GraphDependency+TestData.swift @@ -28,21 +28,31 @@ extension GraphDependency { ) } + public static func testMacro( + path: AbsolutePath = AbsolutePath.root.appending(try! RelativePath(validating: "macro")) + ) -> GraphDependency { + .macro(path: path) + } + public static func testXCFramework( path: AbsolutePath = AbsolutePath.root.appending(try! RelativePath(validating: "Test.xcframework")), infoPlist: XCFrameworkInfoPlist = .test(), primaryBinaryPath: AbsolutePath = AbsolutePath.root .appending(try! RelativePath(validating: "Test.xcframework/Test")), linking: BinaryLinking = .dynamic, - status: FrameworkStatus = .required + status: FrameworkStatus = .required, + macroPath: AbsolutePath? = nil ) -> GraphDependency { .xcframework( - path: path, - infoPlist: infoPlist, - primaryBinaryPath: primaryBinaryPath, - linking: linking, - mergeable: false, - status: status + GraphDependency.XCFramework( + path: path, + infoPlist: infoPlist, + primaryBinaryPath: primaryBinaryPath, + linking: linking, + mergeable: false, + status: status, + macroPath: macroPath + ) ) } @@ -86,6 +96,10 @@ extension GraphDependency { ) } + public static func testBundle(path: AbsolutePath = .root.appending(component: "test.bundle")) -> GraphDependency { + .bundle(path: path) + } + public static func testPackageProduct( path: AbsolutePath = .root, product: String = "Tuist" diff --git a/Sources/TuistGraphTesting/Models/Config+TestData.swift b/Sources/TuistGraphTesting/Models/Config+TestData.swift index 140a697b223..ff2174de0ec 100644 --- a/Sources/TuistGraphTesting/Models/Config+TestData.swift +++ b/Sources/TuistGraphTesting/Models/Config+TestData.swift @@ -30,13 +30,15 @@ extension Config.GenerationOptions { resolveDependenciesWithSystemScm: Bool = false, disablePackageVersionLocking: Bool = false, clonedSourcePackagesDirPath: AbsolutePath? = nil, - staticSideEffectsWarningTargets: TuistGraph.Config.GenerationOptions.StaticSideEffectsWarningTargets = .all + staticSideEffectsWarningTargets: TuistGraph.Config.GenerationOptions.StaticSideEffectsWarningTargets = .all, + enforceExplicitDependencies: Bool = false ) -> Self { .init( resolveDependenciesWithSystemScm: resolveDependenciesWithSystemScm, disablePackageVersionLocking: disablePackageVersionLocking, clonedSourcePackagesDirPath: clonedSourcePackagesDirPath, - staticSideEffectsWarningTargets: staticSideEffectsWarningTargets + staticSideEffectsWarningTargets: staticSideEffectsWarningTargets, + enforceExplicitDependencies: enforceExplicitDependencies ) } } diff --git a/Sources/TuistGraphTesting/Models/Metadata/XCFrameworkMetadata+TestData.swift b/Sources/TuistGraphTesting/Models/Metadata/XCFrameworkMetadata+TestData.swift index 0f525f0aaa0..80ed8ab943e 100644 --- a/Sources/TuistGraphTesting/Models/Metadata/XCFrameworkMetadata+TestData.swift +++ b/Sources/TuistGraphTesting/Models/Metadata/XCFrameworkMetadata+TestData.swift @@ -10,7 +10,8 @@ extension XCFrameworkMetadata { primaryBinaryPath: AbsolutePath = "/XCFrameworks/XCFramework.xcframework/ios-arm64/XCFramework", linking: BinaryLinking = .dynamic, mergeable: Bool = false, - status: FrameworkStatus = .required + status: FrameworkStatus = .required, + macroPath: AbsolutePath? = nil ) -> XCFrameworkMetadata { XCFrameworkMetadata( path: path, @@ -18,7 +19,8 @@ extension XCFrameworkMetadata { primaryBinaryPath: primaryBinaryPath, linking: linking, mergeable: mergeable, - status: status + status: status, + macroPath: macroPath ) } } diff --git a/Sources/TuistGraphTesting/Models/PackageSettings+TestData.swift b/Sources/TuistGraphTesting/Models/PackageSettings+TestData.swift new file mode 100644 index 00000000000..d2bf60fb758 --- /dev/null +++ b/Sources/TuistGraphTesting/Models/PackageSettings+TestData.swift @@ -0,0 +1,21 @@ +import Foundation +import TSCBasic +@testable import TuistGraph + +extension PackageSettings { + public static func test( + productTypes: [String: Product] = [:], + baseSettings: Settings = .test(), + targetSettings: [String: SettingsDictionary] = [:], + projectOptions: [String: TuistGraph.Project.Options] = [:], + platforms: Set = [.iOS] + ) -> PackageSettings { + PackageSettings( + productTypes: productTypes, + baseSettings: baseSettings, + targetSettings: targetSettings, + projectOptions: projectOptions, + platforms: platforms + ) + } +} diff --git a/Sources/TuistGraphTesting/Models/Settings+TestData.swift b/Sources/TuistGraphTesting/Models/Settings+TestData.swift index 50a9955a0f9..dfd95784d11 100644 --- a/Sources/TuistGraphTesting/Models/Settings+TestData.swift +++ b/Sources/TuistGraphTesting/Models/Settings+TestData.swift @@ -13,11 +13,11 @@ extension Configuration { extension Settings { public static func test( - base: SettingsDictionary = [:], + base: SettingsDictionary, // swiftlint:disable:next force_try - debug: Configuration = Configuration(settings: [:], xcconfig: try! AbsolutePath(validating: "/Debug.xcconfig")), + debug: Configuration, // swiftlint:disable:next force_try - release: Configuration = Configuration(settings: [:], xcconfig: try! AbsolutePath(validating: "/Release.xcconfig")) + release: Configuration ) -> Settings { Settings( base: base, @@ -27,9 +27,14 @@ extension Settings { public static func test( base: SettingsDictionary = [:], + baseDebug: SettingsDictionary = [:], configurations: [BuildConfiguration: Configuration?] = [:] ) -> Settings { - Settings(base: base, configurations: configurations) + Settings( + base: base, + baseDebug: baseDebug, + configurations: configurations + ) } public static func test(defaultSettings: DefaultSettings) -> Settings { diff --git a/Sources/TuistGraphTesting/Models/TestAction+TestData.swift b/Sources/TuistGraphTesting/Models/TestAction+TestData.swift index 6ba53e709fe..f5b99df0ff1 100644 --- a/Sources/TuistGraphTesting/Models/TestAction+TestData.swift +++ b/Sources/TuistGraphTesting/Models/TestAction+TestData.swift @@ -18,7 +18,8 @@ extension TestAction { language: String? = nil, region: String? = nil, preferredScreenCaptureFormat: ScreenCaptureFormat? = nil, - testPlans: [TestPlan]? = nil + testPlans: [TestPlan]? = nil, + skippedTests: [String]? = nil ) -> TestAction { TestAction( targets: targets, @@ -34,7 +35,8 @@ extension TestAction { language: language, region: region, preferredScreenCaptureFormat: preferredScreenCaptureFormat, - testPlans: testPlans + testPlans: testPlans, + skippedTests: skippedTests ) } } diff --git a/Sources/TuistKit/Commands/BuildCommand.swift b/Sources/TuistKit/Commands/BuildCommand.swift index 48029c388b7..f83dc09e5b7 100644 --- a/Sources/TuistKit/Commands/BuildCommand.swift +++ b/Sources/TuistKit/Commands/BuildCommand.swift @@ -1,6 +1,7 @@ import ArgumentParser import Foundation import TSCBasic +import TSCUtility import TuistSupport /// Command that builds a target from the project in the current directory. @@ -77,10 +78,10 @@ public struct BuildCommand: AsyncParsableCommand { var derivedDataPath: String? @Flag( - name: [.customLong("raw-xcodebuild-logs")], - help: "When passed, it outputs the raw xcodebuild logs without formatting them." + name: .long, + help: "When passed, it generates the project and skips building. This is useful for debugging purposes." ) - var rawXcodebuildLogs: Bool = false + var generateOnly: Bool = false public func run() async throws { let absolutePath: AbsolutePath @@ -102,7 +103,7 @@ public struct BuildCommand: AsyncParsableCommand { platform: platform, osVersion: os, rosetta: rosetta, - rawXcodebuildLogs: rawXcodebuildLogs + generateOnly: generateOnly ) } } diff --git a/Sources/TuistKit/Commands/CleanCommand.swift b/Sources/TuistKit/Commands/CleanCommand.swift index 241da3462e8..5f9771ca2e1 100644 --- a/Sources/TuistKit/Commands/CleanCommand.swift +++ b/Sources/TuistKit/Commands/CleanCommand.swift @@ -32,8 +32,10 @@ enum CleanCategory: ExpressibleByArgument { } } -struct CleanCommand: ParsableCommand { - static var configuration: CommandConfiguration { +public struct CleanCommand: ParsableCommand { + public init() {} + + public static var configuration: CommandConfiguration { CommandConfiguration( commandName: "clean", abstract: "Clean all the artifacts stored locally" @@ -50,7 +52,7 @@ struct CleanCommand: ParsableCommand { ) var path: String? - func run() throws { + public func run() throws { try CleanService().run( categories: cleanCategories, path: path diff --git a/Sources/TuistKit/Commands/EditCommand.swift b/Sources/TuistKit/Commands/EditCommand.swift index cac4742ef11..649836850c9 100644 --- a/Sources/TuistKit/Commands/EditCommand.swift +++ b/Sources/TuistKit/Commands/EditCommand.swift @@ -4,8 +4,10 @@ import TSCBasic import TuistGenerator import TuistSupport -struct EditCommand: AsyncParsableCommand { - static var configuration: CommandConfiguration { +public struct EditCommand: AsyncParsableCommand { + public init() {} + + public static var configuration: CommandConfiguration { CommandConfiguration( commandName: "edit", abstract: "Generates a temporary project to edit the project in the current directory" @@ -31,7 +33,7 @@ struct EditCommand: AsyncParsableCommand { ) var onlyCurrentDirectory: Bool = false - func run() async throws { + public func run() async throws { try await EditService().run( path: path, permanent: permanent, diff --git a/Sources/TuistKit/Commands/GraphCommand.swift b/Sources/TuistKit/Commands/GraphCommand.swift index 4057a323cba..4b96ec6e764 100644 --- a/Sources/TuistKit/Commands/GraphCommand.swift +++ b/Sources/TuistKit/Commands/GraphCommand.swift @@ -9,10 +9,12 @@ import TuistLoader import TuistSupport /// Command that generates and exports a dot graph from the workspace or project in the current directory. -struct GraphCommand: AsyncParsableCommand, HasTrackableParameters { - static var analyticsDelegate: TrackableParametersDelegate? +public struct GraphCommand: AsyncParsableCommand, HasTrackableParameters { + public init() {} - static var configuration: CommandConfiguration { + public static var analyticsDelegate: TrackableParametersDelegate? + + public static var configuration: CommandConfiguration { CommandConfiguration( commandName: "graph", abstract: "Generates a graph from the workspace or project in the current directory" @@ -71,7 +73,7 @@ struct GraphCommand: AsyncParsableCommand, HasTrackableParameters { ) var outputPath: String? - func run() async throws { + public func run() async throws { GraphCommand.analyticsDelegate?.addParameters( [ "format": AnyCodable(format.rawValue), diff --git a/Sources/TuistKit/Commands/InitCommand.swift b/Sources/TuistKit/Commands/InitCommand.swift index 6fdba46399c..dfe9b4cd607 100644 --- a/Sources/TuistKit/Commands/InitCommand.swift +++ b/Sources/TuistKit/Commands/InitCommand.swift @@ -59,13 +59,13 @@ public struct InitCommand: ParsableCommand, HasTrackableParameters { name = try container.decodeIfPresent(Option.self, forKey: .name)?.wrappedValue template = try container.decodeIfPresent(Option.self, forKey: .template)?.wrappedValue path = try container.decodeIfPresent(Option.self, forKey: .path)?.wrappedValue - try InitCommand.requiredTemplateOptions.forEach { option in + for option in InitCommand.requiredTemplateOptions { requiredTemplateOptions[option.name] = try container.decode( Option.self, forKey: .required(option.name) ).wrappedValue } - try InitCommand.optionalTemplateOptions.forEach { option in + for option in InitCommand.optionalTemplateOptions { optionalTemplateOptions[option.name] = try container.decode( Option.self, forKey: .optional(option.name) diff --git a/Sources/TuistKit/Commands/ListCommand.swift b/Sources/TuistKit/Commands/ListCommand.swift index effc8c7d258..4f59861ca74 100644 --- a/Sources/TuistKit/Commands/ListCommand.swift +++ b/Sources/TuistKit/Commands/ListCommand.swift @@ -1,8 +1,10 @@ import ArgumentParser import Foundation -struct ListCommand: AsyncParsableCommand { - static var configuration: CommandConfiguration { +public struct ListCommand: AsyncParsableCommand { + public init() {} + + public static var configuration: CommandConfiguration { CommandConfiguration( commandName: "list", _superCommandName: "scaffold", @@ -23,7 +25,7 @@ struct ListCommand: AsyncParsableCommand { ) var path: String? - func run() async throws { + public func run() async throws { let format: ListService.OutputFormat = json ? .json : .table try await ListService().run( path: path, diff --git a/Sources/TuistKit/Commands/Migration/MigrationTargetsByDependenciesCommand.swift b/Sources/TuistKit/Commands/Migration/MigrationTargetsByDependenciesCommand.swift index 4d5827f726a..243d9cef8c0 100644 --- a/Sources/TuistKit/Commands/Migration/MigrationTargetsByDependenciesCommand.swift +++ b/Sources/TuistKit/Commands/Migration/MigrationTargetsByDependenciesCommand.swift @@ -3,8 +3,10 @@ import Foundation import TSCBasic import TuistSupport -struct MigrationTargetsByDependenciesCommand: ParsableCommand { - static var configuration: CommandConfiguration { +public struct MigrationTargetsByDependenciesCommand: ParsableCommand { + public init() {} + + public static var configuration: CommandConfiguration { CommandConfiguration( commandName: "list-targets", _superCommandName: "migration", @@ -19,7 +21,7 @@ struct MigrationTargetsByDependenciesCommand: ParsableCommand { ) var xcodeprojPath: String - func run() throws { + public func run() throws { try MigrationTargetsByDependenciesService() .run(xcodeprojPath: try AbsolutePath(validating: xcodeprojPath, relativeTo: FileHandler.shared.currentPath)) } diff --git a/Sources/TuistKit/Commands/Plugin/PluginBuildCommand.swift b/Sources/TuistKit/Commands/Plugin/PluginBuildCommand.swift index f4261e3fd4a..108d8aa80f5 100644 --- a/Sources/TuistKit/Commands/Plugin/PluginBuildCommand.swift +++ b/Sources/TuistKit/Commands/Plugin/PluginBuildCommand.swift @@ -2,8 +2,10 @@ import ArgumentParser import Foundation import TSCBasic -struct PluginBuildCommand: ParsableCommand { - static var configuration: CommandConfiguration { +public struct PluginBuildCommand: ParsableCommand { + public init() {} + + public static var configuration: CommandConfiguration { CommandConfiguration( commandName: "build", abstract: "Builds a plugin." @@ -33,7 +35,7 @@ struct PluginBuildCommand: ParsableCommand { ) var products: [String] = [] - func run() throws { + public func run() throws { try PluginBuildService().run( path: pluginOptions.path, configuration: pluginOptions.configuration, diff --git a/Sources/TuistKit/Commands/Plugin/PluginRunCommand.swift b/Sources/TuistKit/Commands/Plugin/PluginRunCommand.swift index 4a4503b938f..34c27eebc4a 100644 --- a/Sources/TuistKit/Commands/Plugin/PluginRunCommand.swift +++ b/Sources/TuistKit/Commands/Plugin/PluginRunCommand.swift @@ -2,8 +2,10 @@ import ArgumentParser import Foundation import TSCBasic -struct PluginRunCommand: ParsableCommand { - static var configuration: CommandConfiguration { +public struct PluginRunCommand: ParsableCommand { + public init() {} + + public static var configuration: CommandConfiguration { CommandConfiguration( commandName: "run", abstract: "Runs a plugin." @@ -33,7 +35,7 @@ struct PluginRunCommand: ParsableCommand { ) var arguments: [String] = [] - func run() throws { + public func run() throws { try PluginRunService().run( path: pluginOptions.path, configuration: pluginOptions.configuration, diff --git a/Sources/TuistKit/Commands/Plugin/PluginTestCommand.swift b/Sources/TuistKit/Commands/Plugin/PluginTestCommand.swift index 9a41044cef1..76da4a1cdf5 100644 --- a/Sources/TuistKit/Commands/Plugin/PluginTestCommand.swift +++ b/Sources/TuistKit/Commands/Plugin/PluginTestCommand.swift @@ -2,8 +2,10 @@ import ArgumentParser import Foundation import TSCBasic -struct PluginTestCommand: ParsableCommand { - static var configuration: CommandConfiguration { +public struct PluginTestCommand: ParsableCommand { + public init() {} + + public static var configuration: CommandConfiguration { CommandConfiguration( commandName: "test", abstract: "Tests a plugin." @@ -23,7 +25,7 @@ struct PluginTestCommand: ParsableCommand { ) var testProducts: [String] = [] - func run() throws { + public func run() throws { try PluginTestService().run( path: pluginOptions.path, configuration: pluginOptions.configuration, diff --git a/Sources/TuistKit/Commands/RunCommand.swift b/Sources/TuistKit/Commands/RunCommand.swift index e6829f0fc80..44c72e4ecd4 100644 --- a/Sources/TuistKit/Commands/RunCommand.swift +++ b/Sources/TuistKit/Commands/RunCommand.swift @@ -1,10 +1,13 @@ import ArgumentParser import Foundation import TSCBasic +import TSCUtility import TuistSupport -struct RunCommand: AsyncParsableCommand { - static var configuration: CommandConfiguration { +public struct RunCommand: AsyncParsableCommand { + public init() {} + + public static var configuration: CommandConfiguration { CommandConfiguration( commandName: "run", abstract: "Runs a scheme or target in the project", @@ -64,13 +67,7 @@ struct RunCommand: AsyncParsableCommand { ) var arguments: [String] = [] - @Flag( - name: [.customLong("raw-xcodebuild-logs")], - help: "When passed, it outputs the raw xcodebuild logs without formatting them." - ) - var rawXcodebuildLogs: Bool = false - - func run() async throws { + public func run() async throws { try await RunService().run( path: path, schemeName: scheme, @@ -80,8 +77,7 @@ struct RunCommand: AsyncParsableCommand { device: device, version: os, rosetta: rosetta, - arguments: arguments, - rawXcodebuildLogs: rawXcodebuildLogs + arguments: arguments ) } } diff --git a/Sources/TuistKit/Commands/ScaffoldCommand.swift b/Sources/TuistKit/Commands/ScaffoldCommand.swift index 01134a45cb4..be00c8f8ca9 100644 --- a/Sources/TuistKit/Commands/ScaffoldCommand.swift +++ b/Sources/TuistKit/Commands/ScaffoldCommand.swift @@ -25,8 +25,8 @@ enum ScaffoldCommandError: FatalError, Equatable { } } -struct ScaffoldCommand: AsyncParsableCommand { - static var configuration: CommandConfiguration { +public struct ScaffoldCommand: AsyncParsableCommand { + public static var configuration: CommandConfiguration { CommandConfiguration( commandName: "scaffold", abstract: "Generates new project based on a template", @@ -60,22 +60,22 @@ struct ScaffoldCommand: AsyncParsableCommand { var requiredTemplateOptions: [String: String] = [:] var optionalTemplateOptions: [String: String?] = [:] - init() {} + public init() {} // Custom decoding to decode dynamic options - init(from decoder: Decoder) throws { + public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) template = try container.decode(Argument.self, forKey: .template).wrappedValue json = try container.decodeIfPresent(Option.self, forKey: .json)?.wrappedValue ?? false path = try container.decodeIfPresent(Option.self, forKey: .path)?.wrappedValue url = try container.decodeIfPresent(Option.self, forKey: .url)?.wrappedValue - try ScaffoldCommand.requiredTemplateOptions.forEach { option in + for option in ScaffoldCommand.requiredTemplateOptions { requiredTemplateOptions[option.name] = try container.decode( Option.self, forKey: .required(option.name) ).wrappedValue } - try ScaffoldCommand.optionalTemplateOptions.forEach { option in + for option in ScaffoldCommand.optionalTemplateOptions { optionalTemplateOptions[option.name] = try container.decode( Option.self, forKey: .optional(option.name) @@ -83,7 +83,7 @@ struct ScaffoldCommand: AsyncParsableCommand { } } - func run() async throws { + public func run() async throws { // Currently, @Argument and subcommand clashes, so we need to handle that ourselves if template == ListCommand.configuration.commandName { let format: ListService.OutputFormat = json ? .json : .table @@ -103,11 +103,11 @@ struct ScaffoldCommand: AsyncParsableCommand { // MARK: - Preprocessing extension ScaffoldCommand { - static var requiredTemplateOptions: [(name: String, option: Option)] = [] - static var optionalTemplateOptions: [(name: String, option: Option)] = [] + public static var requiredTemplateOptions: [(name: String, option: Option)] = [] + public static var optionalTemplateOptions: [(name: String, option: Option)] = [] /// We do not know template's option in advance -> we need to dynamically add them - static func preprocess(_ arguments: [String]? = nil) async throws { + public static func preprocess(_ arguments: [String]? = nil) async throws { guard let arguments, arguments.count >= 2 else { throw ScaffoldCommandError.templateNotProvided } @@ -198,7 +198,7 @@ extension ScaffoldCommand { /// ArgumentParser library gets the list of options from a mirror /// Since we do not declare template's options in advance, we need to rewrite the mirror implementation and add them ourselves extension ScaffoldCommand: CustomReflectable { - var customMirror: Mirror { + public var customMirror: Mirror { let requiredTemplateChildren = ScaffoldCommand.requiredTemplateOptions .map { Mirror.Child(label: $0.name, value: $0.option) } let optionalTemplateChildren = ScaffoldCommand.optionalTemplateOptions diff --git a/Sources/TuistKit/Commands/TestCommand.swift b/Sources/TuistKit/Commands/TestCommand.swift index cf8a7fa9a2c..eb8a0f1af17 100644 --- a/Sources/TuistKit/Commands/TestCommand.swift +++ b/Sources/TuistKit/Commands/TestCommand.swift @@ -125,10 +125,10 @@ public struct TestCommand: AsyncParsableCommand, HasTrackableParameters { var skipConfigurations: [String] = [] @Flag( - name: [.customLong("raw-xcodebuild-logs")], - help: "When passed, it outputs the raw xcodebuild logs without formatting them." + name: .long, + help: "When passed, it generates the project and skips testing. This is useful for debugging purposes." ) - var rawXcodebuildLogs: Bool = false + var generateOnly: Bool = false public func validate() throws { try TestService().validateParameters( @@ -174,7 +174,7 @@ public struct TestCommand: AsyncParsableCommand, HasTrackableParameters { ) }, validateTestTargetsParameters: false, - rawXcodebuildLogs: rawXcodebuildLogs + generateOnly: generateOnly ) } } diff --git a/Sources/TuistKit/Generator/GeneratorFactory.swift b/Sources/TuistKit/Generator/GeneratorFactory.swift index 6472312ae68..aafedc9e247 100644 --- a/Sources/TuistKit/Generator/GeneratorFactory.swift +++ b/Sources/TuistKit/Generator/GeneratorFactory.swift @@ -26,7 +26,9 @@ protocol GeneratorFactorying { /// Returns the default generator. /// - Parameter config: The project configuration. /// - Returns: A Generator instance. - func `default`() -> Generating + func `default`( + config: Config + ) -> Generating } public class GeneratorFactory: GeneratorFactorying { @@ -69,13 +71,13 @@ public class GeneratorFactory: GeneratorFactorying { ) } - public func `default`() -> Generating { + public func `default`(config: Config) -> Generating { let contentHasher = ContentHasher() let projectMapperFactory = ProjectMapperFactory(contentHasher: contentHasher) let projectMappers = projectMapperFactory.default() let workspaceMapperFactory = WorkspaceMapperFactory(projectMapper: SequentialProjectMapper(mappers: projectMappers)) let graphMapperFactory = GraphMapperFactory() - let graphMappers = graphMapperFactory.default() + let graphMappers = graphMapperFactory.default(config: config) let workspaceMappers = workspaceMapperFactory.default() let manifestLoader = ManifestLoaderFactory().createManifestLoader() return Generator( diff --git a/Sources/TuistKit/Log/Logger.swift b/Sources/TuistKit/Log/Logger.swift index 3e18e7b5c34..20bce74e86c 100644 --- a/Sources/TuistKit/Log/Logger.swift +++ b/Sources/TuistKit/Log/Logger.swift @@ -1,2 +1,3 @@ import TuistSupport + let logger = Logger(label: "io.tuist") diff --git a/Sources/TuistKit/Mappers/Factories/GraphMapperFactory.swift b/Sources/TuistKit/Mappers/Factories/GraphMapperFactory.swift index 696baf040fc..ae61d37184c 100644 --- a/Sources/TuistKit/Mappers/Factories/GraphMapperFactory.swift +++ b/Sources/TuistKit/Mappers/Factories/GraphMapperFactory.swift @@ -1,6 +1,7 @@ import Foundation import TSCBasic import TuistCore +import TuistDependencies import TuistGenerator import TuistGraph import TuistSigning @@ -20,14 +21,16 @@ protocol GraphMapperFactorying { /// Returns the default graph mapper that should be used from all the commands that require loading and processing the graph. /// - Returns: The default mapper. - func `default`() -> [GraphMapping] + func `default`( + config: Config + ) -> [GraphMapping] } public final class GraphMapperFactory: GraphMapperFactorying { public init() {} public func automation( - config _: Config, + config: Config, testsCacheDirectory _: AbsolutePath, testPlan: String?, includedTargets: Set, @@ -42,14 +45,21 @@ public final class GraphMapperFactory: GraphMapperFactorying { ) ) mappers.append(TreeShakePrunedTargetsGraphMapper()) - mappers.append(contentsOf: self.default()) + mappers.append(contentsOf: self.default(config: config)) return mappers } - func `default`() -> [GraphMapping] { + public func `default`( + config: Config + ) -> [GraphMapping] { var mappers: [GraphMapping] = [] mappers.append(UpdateWorkspaceProjectsGraphMapper()) + mappers.append(PruneOrphanExternalTargetsGraphMapper()) + mappers.append(ExternalProjectsPlatformNarrowerGraphMapper()) + if config.generationOptions.enforceExplicitDependencies { + mappers.append(ExplicitDependencyGraphMapper()) + } return mappers } } diff --git a/Sources/TuistKit/Mappers/FocusTargetsGraphMappers.swift b/Sources/TuistKit/Mappers/FocusTargetsGraphMappers.swift index aee5e8aab65..5cc20700aac 100644 --- a/Sources/TuistKit/Mappers/FocusTargetsGraphMappers.swift +++ b/Sources/TuistKit/Mappers/FocusTargetsGraphMappers.swift @@ -34,10 +34,10 @@ public final class FocusTargetsGraphMappers: GraphMapping { let filteredTargets = Set(try topologicalSort( Array(userSpecifiedSourceTargets), - successors: { Array(graphTraverser.directTargetDependencies(path: $0.path, name: $0.target.name)) } + successors: { Array(graphTraverser.directTargetDependencies(path: $0.path, name: $0.target.name)).map(\.graphTarget) } )) - graphTraverser.allTargets().forEach { graphTarget in + for graphTarget in graphTraverser.allTargets() { if !filteredTargets.contains(graphTarget) { var target = graphTarget.target target.prune = true diff --git a/Sources/TuistKit/ProjectEditor/ProjectEditor.swift b/Sources/TuistKit/ProjectEditor/ProjectEditor.swift index f50b05b0b50..8275697272d 100644 --- a/Sources/TuistKit/ProjectEditor/ProjectEditor.swift +++ b/Sources/TuistKit/ProjectEditor/ProjectEditor.swift @@ -137,6 +137,7 @@ final class ProjectEditor: ProjectEditing { cacheDirectory: cacheDirectory.cacheDirectory(for: .projectDescriptionHelpers) ) let dependenciesPath = manifestFilesLocator.locateDependencies(at: editingPath) + let packageManifestPath = manifestFilesLocator.locatePackageManifest(at: editingPath) let helpers = helpersDirectoryLocator.locate(at: editingPath).map { [ @@ -192,6 +193,7 @@ final class ProjectEditor: ProjectEditing { destinationDirectory: destinationDirectory, configPath: configPath, dependenciesPath: dependenciesPath, + packageManifestPath: packageManifestPath, projectManifests: projectManifests.map(\.path), editablePluginManifests: editablePluginManifests, pluginProjectDescriptionHelpersModule: builtPluginHelperModules, diff --git a/Sources/TuistKit/ProjectEditor/ProjectEditorMapper.swift b/Sources/TuistKit/ProjectEditor/ProjectEditorMapper.swift index 269a9af63da..341f597c266 100644 --- a/Sources/TuistKit/ProjectEditor/ProjectEditorMapper.swift +++ b/Sources/TuistKit/ProjectEditor/ProjectEditorMapper.swift @@ -13,6 +13,7 @@ protocol ProjectEditorMapping: AnyObject { destinationDirectory: AbsolutePath, configPath: AbsolutePath?, dependenciesPath: AbsolutePath?, + packageManifestPath: AbsolutePath?, projectManifests: [AbsolutePath], editablePluginManifests: [EditablePluginManifest], pluginProjectDescriptionHelpersModule: [ProjectDescriptionHelpersModule], @@ -27,6 +28,14 @@ protocol ProjectEditorMapping: AnyObject { // swiftlint:disable:next type_body_length final class ProjectEditorMapper: ProjectEditorMapping { + private let swiftPackageManagerController: SwiftPackageManagerControlling + + init( + swiftPackageManagerController: SwiftPackageManagerControlling = SwiftPackageManagerController() + ) { + self.swiftPackageManagerController = swiftPackageManagerController + } + // swiftlint:disable:next function_body_length func map( name: String, @@ -35,6 +44,7 @@ final class ProjectEditorMapper: ProjectEditorMapping { destinationDirectory: AbsolutePath, configPath: AbsolutePath?, dependenciesPath: AbsolutePath?, + packageManifestPath: AbsolutePath?, projectManifests: [AbsolutePath], editablePluginManifests: [EditablePluginManifest], pluginProjectDescriptionHelpersModule: [ProjectDescriptionHelpersModule], @@ -56,7 +66,7 @@ final class ProjectEditorMapper: ProjectEditorMapping { tuistPath: tuistPath ) - let manifestsProject = mapManifestsProject( + let manifestsProject = try mapManifestsProject( projectManifests: projectManifests, projectDescriptionPath: projectDescriptionSearchPath, swiftVersion: swiftVersion, @@ -70,6 +80,7 @@ final class ProjectEditorMapper: ProjectEditorMapping { stencils: stencils, configPath: configPath, dependenciesPath: dependenciesPath, + packageManifestPath: packageManifestPath, editablePluginTargets: editablePluginManifests.map(\.name), pluginProjectDescriptionHelpersModule: pluginProjectDescriptionHelpersModule ) @@ -146,9 +157,10 @@ final class ProjectEditorMapper: ProjectEditorMapping { stencils: [AbsolutePath], configPath: AbsolutePath?, dependenciesPath: AbsolutePath?, + packageManifestPath: AbsolutePath?, editablePluginTargets: [String], pluginProjectDescriptionHelpersModule: [ProjectDescriptionHelpersModule] - ) -> Project? { + ) throws -> Project? { guard !projectManifests.isEmpty else { return nil } let projectName = "Manifests" @@ -246,6 +258,48 @@ final class ProjectEditorMapper: ProjectEditorMapping { ) }() + let packagesTarget: Target? = try { + guard let packageManifestPath, + let xcode = try XcodeController.shared.selected() + else { return nil } + let packageVersion = try swiftPackageManagerController.getToolsVersion(at: packageManifestPath.parentDirectory) + + var packagesSettings = targetBaseSettings( + projectFrameworkPath: projectDescriptionPath, + pluginHelperLibraryPaths: pluginProjectDescriptionHelpersModule.map(\.path), + swiftVersion: swiftVersion + ) + packagesSettings.merge( + [ + "OTHER_SWIFT_FLAGS": .array([ + "-package-description-version", + packageVersion.description, + "-D", "TUIST", + ]), + "SWIFT_INCLUDE_PATHS": .array([ + "\(xcode.path.pathString)/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/pm/ManifestAPI", + ]), + ], + uniquingKeysWith: { $1 } + ) + + let dependencies: [TargetDependency] = helpersTarget == nil ? [] : [ + .target(name: "ProjectDescriptionHelpers"), + ] + + return editorHelperTarget( + name: "Packages", + filesGroup: manifestsFilesGroup, + targetSettings: Settings( + base: packagesSettings, + configurations: Settings.default.configurations, + defaultSettings: .recommended + ), + sourcePaths: [packageManifestPath], + dependencies: dependencies + ) + }() + let manifestsTargets = namedManifests(projectManifests).map { name, projectManifestSourcePath -> Target in editorHelperTarget( name: name, @@ -263,6 +317,7 @@ final class ProjectEditorMapper: ProjectEditorMapping { stencilsTarget, configTarget, dependenciesTarget, + packagesTarget, ] .compactMap { $0 } + manifestsTargets diff --git a/Sources/TuistKit/Services/BuildService.swift b/Sources/TuistKit/Services/BuildService.swift index fbd63a834f5..0de0083b515 100644 --- a/Sources/TuistKit/Services/BuildService.swift +++ b/Sources/TuistKit/Services/BuildService.swift @@ -37,15 +37,18 @@ final class BuildService { private let generatorFactory: GeneratorFactorying private let buildGraphInspector: BuildGraphInspecting private let targetBuilder: TargetBuilding + private let configLoader: ConfigLoading init( generatorFactory: GeneratorFactorying = GeneratorFactory(), buildGraphInspector: BuildGraphInspecting = BuildGraphInspector(), - targetBuilder: TargetBuilding = TargetBuilder() + targetBuilder: TargetBuilding = TargetBuilder(), + configLoader: ConfigLoading = ConfigLoader(manifestLoader: ManifestLoader()) ) { self.generatorFactory = generatorFactory self.buildGraphInspector = buildGraphInspector self.targetBuilder = targetBuilder + self.configLoader = configLoader } // swiftlint:disable:next function_body_length @@ -61,16 +64,21 @@ final class BuildService { platform: String?, osVersion: String?, rosetta: Bool, - rawXcodebuildLogs: Bool + generateOnly: Bool ) async throws { let graph: Graph - let generator = generatorFactory.default() + let config = try configLoader.loadConfig(path: path) + let generator = generatorFactory.default(config: config) if try (generate || buildGraphInspector.workspacePath(directory: path) == nil) { graph = try await generator.generateWithGraph(path: path).1 } else { graph = try await generator.load(path: path) } + if generateOnly { + return + } + guard let workspacePath = try buildGraphInspector.workspacePath(directory: path) else { throw BuildServiceError.workspaceNotFound(path: path.pathString) } @@ -119,8 +127,7 @@ final class BuildService { device: device, osVersion: osVersion?.version(), rosetta: rosetta, - graphTraverser: graphTraverser, - rawXcodebuildLogs: rawXcodebuildLogs + graphTraverser: graphTraverser ) } else { var cleaned = false @@ -151,8 +158,7 @@ final class BuildService { device: device, osVersion: osVersion?.version(), rosetta: rosetta, - graphTraverser: graphTraverser, - rawXcodebuildLogs: rawXcodebuildLogs + graphTraverser: graphTraverser ) cleaned = true } diff --git a/Sources/TuistKit/Services/CleanService.swift b/Sources/TuistKit/Services/CleanService.swift index 0262ceb9ee8..860dcbe9b9b 100644 --- a/Sources/TuistKit/Services/CleanService.swift +++ b/Sources/TuistKit/Services/CleanService.swift @@ -24,8 +24,8 @@ final class CleanService { let config = try configLoader.loadConfig(path: path) let cacheDirectoryProvider = try cacheDirectoryProviderFactory.cacheDirectories(config: config) - try categories.forEach { - switch $0 { + for category in categories { + switch category { case let .global(cacheCategory): try cleanCacheCategory( cacheCategory, diff --git a/Sources/TuistKit/Services/FetchService.swift b/Sources/TuistKit/Services/FetchService.swift index d1f849bc228..ffc7ce08f43 100644 --- a/Sources/TuistKit/Services/FetchService.swift +++ b/Sources/TuistKit/Services/FetchService.swift @@ -13,7 +13,10 @@ final class FetchService { private let manifestLoader: ManifestLoading private let dependenciesController: DependenciesControlling private let dependenciesModelLoader: DependenciesModelLoading + private let packageSettingsLoader: PackageSettingsLoading private let converter: ManifestModelConverting + private let rootDirectoryLocator: RootDirectoryLocating + private let fileHandler: FileHandling init( pluginService: PluginServicing = PluginService(), @@ -21,27 +24,49 @@ final class FetchService { manifestLoader: ManifestLoading = ManifestLoader(), dependenciesController: DependenciesControlling = DependenciesController(), dependenciesModelLoader: DependenciesModelLoading = DependenciesModelLoader(), - converter: ManifestModelConverting = ManifestModelConverter() + packageSettingsLoader: PackageSettingsLoading = PackageSettingsLoader(), + converter: ManifestModelConverting = ManifestModelConverter(), + rootDirectoryLocator: RootDirectoryLocating = RootDirectoryLocator(), + fileHandler: FileHandling = FileHandler.shared ) { self.pluginService = pluginService self.configLoader = configLoader self.manifestLoader = manifestLoader self.dependenciesController = dependenciesController self.dependenciesModelLoader = dependenciesModelLoader + self.packageSettingsLoader = packageSettingsLoader self.converter = converter + self.rootDirectoryLocator = rootDirectoryLocator + self.fileHandler = fileHandler } func run( path: String?, update: Bool ) async throws { - let path = try self.path(path) - + let path = try locateDependencies(at: path) try await fetchDependencies(path: path, update: update, with: fetchPlugins(path: path)) } // MARK: - Helpers + public func locateDependencies(at path: String?) throws -> AbsolutePath { + // Convert to AbsolutePath + let path = try self.path(path) + + // If the Dependencies.swift file exists in the root Tuist directory, we load it from there + if let rootDirectoryPath = rootDirectoryLocator.locate(from: path) { + if fileHandler.exists( + rootDirectoryPath.appending(components: Constants.tuistDirectoryName, Manifest.dependencies.fileName(path)) + ) { + return rootDirectoryPath + } + } + + // Otherwise return the original path + return path + } + private func path(_ path: String?) throws -> AbsolutePath { if let path { return try AbsolutePath(validating: path, relativeTo: currentPath) @@ -51,7 +76,7 @@ final class FetchService { } private var currentPath: AbsolutePath { - FileHandler.shared.currentPath + fileHandler.currentPath } private func fetchPlugins(path: AbsolutePath) async throws -> TuistGraph.Plugins { @@ -68,9 +93,16 @@ final class FetchService { private func fetchDependencies(path: AbsolutePath, update: Bool, with plugins: TuistGraph.Plugins) throws { try manifestLoader.validateHasProjectOrWorkspaceManifest(at: path) - guard FileHandler.shared.exists( - path.appending(components: Constants.tuistDirectoryName, Manifest.dependencies.fileName(path)) - ) else { + let dependenciesManifestPath = path.appending( + components: Constants.tuistDirectoryName, + Manifest.dependencies.fileName(path) + ) + let packageManifestPath = path.appending( + components: Constants.tuistDirectoryName, + Constants.DependenciesDirectory.packageSwiftName + ) + + guard fileHandler.exists(dependenciesManifestPath) || fileHandler.exists(packageManifestPath) else { return } @@ -80,24 +112,43 @@ final class FetchService { logger.info("Resolving and fetching dependencies.", metadata: .section) } - let dependencies = try dependenciesModelLoader.loadDependencies(at: path, with: plugins) - let config = try configLoader.loadConfig(path: path) let swiftVersion = config.swiftVersion let dependenciesManifest: TuistCore.DependenciesGraph - if update { - dependenciesManifest = try dependenciesController.update( - at: path, - dependencies: dependencies, - swiftVersion: swiftVersion - ) + if fileHandler.exists(dependenciesManifestPath) { + let dependencies = try dependenciesModelLoader.loadDependencies(at: path, with: plugins) + + if update { + dependenciesManifest = try dependenciesController.update( + at: path, + dependencies: dependencies, + swiftVersion: swiftVersion + ) + } else { + dependenciesManifest = try dependenciesController.fetch( + at: path, + dependencies: dependencies, + swiftVersion: swiftVersion + ) + } + } else { - dependenciesManifest = try dependenciesController.fetch( - at: path, - dependencies: dependencies, - swiftVersion: swiftVersion - ) + let packageSettings = try packageSettingsLoader.loadPackageSettings(at: path, with: plugins) + + if update { + dependenciesManifest = try dependenciesController.update( + at: path, + packageSettings: packageSettings, + swiftVersion: swiftVersion + ) + } else { + dependenciesManifest = try dependenciesController.fetch( + at: path, + packageSettings: packageSettings, + swiftVersion: swiftVersion + ) + } } let dependenciesGraph = try converter.convert(manifest: dependenciesManifest, path: path) diff --git a/Sources/TuistKit/Services/GenerateService.swift b/Sources/TuistKit/Services/GenerateService.swift index 893f6df637d..0993e77ce6f 100644 --- a/Sources/TuistKit/Services/GenerateService.swift +++ b/Sources/TuistKit/Services/GenerateService.swift @@ -14,6 +14,7 @@ final class GenerateService { private let generatorFactory: GeneratorFactorying private let manifestLoader: ManifestLoading private let pluginService: PluginServicing + private let configLoader: ConfigLoading init( clock: Clock = WallClock(), @@ -21,7 +22,8 @@ final class GenerateService { manifestLoader: ManifestLoading = ManifestLoader(), opener: Opening = Opener(), generatorFactory: GeneratorFactorying = GeneratorFactory(), - pluginService: PluginServicing = PluginService() + pluginService: PluginServicing = PluginService(), + configLoader: ConfigLoading = ConfigLoader(manifestLoader: ManifestLoader()) ) { self.clock = clock self.timeTakenLoggerFormatter = timeTakenLoggerFormatter @@ -29,6 +31,7 @@ final class GenerateService { self.opener = opener self.generatorFactory = generatorFactory self.pluginService = pluginService + self.configLoader = configLoader } func run( @@ -37,7 +40,8 @@ final class GenerateService { ) async throws { let timer = clock.startTimer() let path = try self.path(path) - let generator = generatorFactory.default() + let config = try configLoader.loadConfig(path: path) + let generator = generatorFactory.default(config: config) let workspacePath = try await generator.generate(path: path) if !noOpen { try opener.open(path: workspacePath) diff --git a/Sources/TuistKit/Services/Plugin/PluginArchiveService.swift b/Sources/TuistKit/Services/Plugin/PluginArchiveService.swift index 7b1eb5ad2be..a81027649f9 100644 --- a/Sources/TuistKit/Services/Plugin/PluginArchiveService.swift +++ b/Sources/TuistKit/Services/Plugin/PluginArchiveService.swift @@ -71,16 +71,15 @@ final class PluginArchiveService { in temporaryDirectory: AbsolutePath ) throws { let artifactsPath = temporaryDirectory.appending(component: "artifacts") - try taskProducts - .forEach { product in - logger.notice("Building \(product)...") - try swiftPackageManagerController.buildFatReleaseBinary( - packagePath: path, - product: product, - buildPath: temporaryDirectory.appending(component: "build"), - outputPath: artifactsPath - ) - } + for product in taskProducts { + logger.notice("Building \(product)...") + try swiftPackageManagerController.buildFatReleaseBinary( + packagePath: path, + product: product, + buildPath: temporaryDirectory.appending(component: "build"), + outputPath: artifactsPath + ) + } let archiver = try fileArchiverFactory.makeFileArchiver( for: taskProducts .map(artifactsPath.appending) diff --git a/Sources/TuistKit/Services/Plugin/PluginBuildService.swift b/Sources/TuistKit/Services/Plugin/PluginBuildService.swift index a95195d2703..29e0f208a79 100644 --- a/Sources/TuistKit/Services/Plugin/PluginBuildService.swift +++ b/Sources/TuistKit/Services/Plugin/PluginBuildService.swift @@ -30,14 +30,14 @@ final class PluginBuildService { "--show-bin-path" ) } - targets.forEach { + for target in targets { buildCommand += [ - "--target", $0, + "--target", target, ] } - products.forEach { + for product in products { buildCommand += [ - "--product", $0, + "--product", product, ] } try System.shared.runAndPrint(buildCommand) diff --git a/Sources/TuistKit/Services/Plugin/PluginTestService.swift b/Sources/TuistKit/Services/Plugin/PluginTestService.swift index e71eeee86ca..b761322eefc 100644 --- a/Sources/TuistKit/Services/Plugin/PluginTestService.swift +++ b/Sources/TuistKit/Services/Plugin/PluginTestService.swift @@ -23,9 +23,9 @@ final class PluginTestService { "--build-tests" ) } - testProducts.forEach { + for testProduct in testProducts { testCommand += [ - "--test-product", $0, + "--test-product", testProduct, ] } try System.shared.runAndPrint(testCommand) diff --git a/Sources/TuistKit/Services/RunService.swift b/Sources/TuistKit/Services/RunService.swift index 4c70c985f47..3561bccc16b 100644 --- a/Sources/TuistKit/Services/RunService.swift +++ b/Sources/TuistKit/Services/RunService.swift @@ -43,17 +43,20 @@ final class RunService { private let buildGraphInspector: BuildGraphInspecting private let targetBuilder: TargetBuilding private let targetRunner: TargetRunning + private let configLoader: ConfigLoading init( generatorFactory: GeneratorFactorying = GeneratorFactory(), buildGraphInspector: BuildGraphInspecting = BuildGraphInspector(), targetBuilder: TargetBuilding = TargetBuilder(), - targetRunner: TargetRunning = TargetRunner() + targetRunner: TargetRunning = TargetRunner(), + configLoader: ConfigLoading = ConfigLoader(manifestLoader: ManifestLoader()) ) { self.generatorFactory = generatorFactory self.buildGraphInspector = buildGraphInspector self.targetBuilder = targetBuilder self.targetRunner = targetRunner + self.configLoader = configLoader } // swiftlint:disable:next function_body_length @@ -66,8 +69,7 @@ final class RunService { device: String?, version: String?, rosetta: Bool, - arguments: [String], - rawXcodebuildLogs: Bool + arguments: [String] ) async throws { let runPath: AbsolutePath if let path { @@ -77,7 +79,8 @@ final class RunService { } let graph: Graph - let generator = generatorFactory.default() + let config = try configLoader.loadConfig(path: runPath) + let generator = generatorFactory.default(config: config) if try (generate || buildGraphInspector.workspacePath(directory: runPath) == nil) { logger.notice("Generating project for running", metadata: .section) graph = try await generator.generateWithGraph(path: runPath).1 @@ -116,8 +119,7 @@ final class RunService { device: device, osVersion: version?.version(), rosetta: rosetta, - graphTraverser: graphTraverser, - rawXcodebuildLogs: rawXcodebuildLogs + graphTraverser: graphTraverser ) let minVersion: Version? diff --git a/Sources/TuistKit/Services/TestService.swift b/Sources/TuistKit/Services/TestService.swift index 5c141652d1c..e8c4ad5c8bd 100644 --- a/Sources/TuistKit/Services/TestService.swift +++ b/Sources/TuistKit/Services/TestService.swift @@ -170,7 +170,7 @@ public final class TestService { // swiftlint:disable:this type_body_length testPlanConfiguration: TestPlanConfiguration?, validateTestTargetsParameters: Bool = true, generator: Generating? = nil, - rawXcodebuildLogs: Bool + generateOnly: Bool ) async throws { if validateTestTargetsParameters { try validateParameters( @@ -183,7 +183,6 @@ public final class TestService { // swiftlint:disable:this type_body_length let manifestLoader = manifestLoaderFactory.createManifestLoader() let configLoader = ConfigLoader(manifestLoader: manifestLoader) let config = try configLoader.loadConfig(path: path) - let cacheDirectoriesProvider = try cacheDirectoryProviderFactory.cacheDirectories(config: config) let testGenerator: Generating if let generator { @@ -194,7 +193,7 @@ public final class TestService { // swiftlint:disable:this type_body_length testsCacheDirectory: testsCacheTemporaryDirectory.path, testPlan: testPlanConfiguration?.testPlan, includedTargets: Set(testTargets.map(\.target)), - excludedTargets: Set(skipTestTargets.map(\.target)), + excludedTargets: Set(skipTestTargets.filter { $0.class == nil }.map(\.target)), skipUITests: skipUITests ) } @@ -203,6 +202,11 @@ public final class TestService { // swiftlint:disable:this type_body_length let graph = try await testGenerator.generateWithGraph( path: path ).1 + + if generateOnly { + return + } + let graphTraverser = GraphTraverser(graph: graph) let version = osVersion?.version() let testableSchemes = buildGraphInspector.testableSchemes(graphTraverser: graphTraverser) + @@ -256,8 +260,7 @@ public final class TestService { // swiftlint:disable:this type_body_length retryCount: retryCount, testTargets: testTargets, skipTestTargets: skipTestTargets, - testPlanConfiguration: testPlanConfiguration, - rawXcodebuildLogs: rawXcodebuildLogs + testPlanConfiguration: testPlanConfiguration ) } } else { @@ -286,36 +289,17 @@ public final class TestService { // swiftlint:disable:this type_body_length retryCount: retryCount, testTargets: testTargets, skipTestTargets: skipTestTargets, - testPlanConfiguration: testPlanConfiguration, - rawXcodebuildLogs: rawXcodebuildLogs + testPlanConfiguration: testPlanConfiguration ) } } - // Saving hashes from `testsCacheTemporaryDirectory` to `testsCacheDirectory` after all the tests have run successfully - - if !FileHandler.shared.exists( - cacheDirectoriesProvider.cacheDirectory(for: .tests) - ) { - try FileHandler.shared.createFolder(cacheDirectoriesProvider.cacheDirectory(for: .tests)) - } - - try FileHandler.shared - .contentsOfDirectory(testsCacheTemporaryDirectory.path) - .forEach { hashPath in - let destination = cacheDirectoriesProvider.cacheDirectory(for: .tests).appending(component: hashPath.basename) - guard !FileHandler.shared.exists(destination) else { return } - try FileHandler.shared.move( - from: hashPath, - to: destination - ) - } - logger.log(level: .notice, "The project tests ran successfully", metadata: .success) } // MARK: - Helpers + // swiftlint:disable:next function_body_length private func testScheme( scheme: Scheme, graphTraverser: GraphTraversing, @@ -330,8 +314,7 @@ public final class TestService { // swiftlint:disable:this type_body_length retryCount: Int, testTargets: [TestIdentifier], skipTestTargets: [TestIdentifier], - testPlanConfiguration: TestPlanConfiguration?, - rawXcodebuildLogs: Bool + testPlanConfiguration: TestPlanConfiguration? ) async throws { logger.log(level: .notice, "Testing scheme \(scheme.name)", metadata: .section) if let testPlan = testPlanConfiguration?.testPlan, let testPlans = scheme.testAction?.testPlans, @@ -388,8 +371,7 @@ public final class TestService { // swiftlint:disable:this type_body_length retryCount: retryCount, testTargets: testTargets, skipTestTargets: skipTestTargets, - testPlanConfiguration: testPlanConfiguration, - rawXcodebuildLogs: rawXcodebuildLogs + testPlanConfiguration: testPlanConfiguration ) .printFormattedOutput() } diff --git a/Sources/TuistLoader/Loaders/CachedManifestLoader.swift b/Sources/TuistLoader/Loaders/CachedManifestLoader.swift index 6421d5e6684..5e329fbcb2d 100644 --- a/Sources/TuistLoader/Loaders/CachedManifestLoader.swift +++ b/Sources/TuistLoader/Loaders/CachedManifestLoader.swift @@ -94,6 +94,10 @@ public class CachedManifestLoader: ManifestLoading { try manifestLoader.loadDependencies(at: path) } + public func loadPackageSettings(at path: AbsolutePath) throws -> PackageSettings { + try manifestLoader.loadPackageSettings(at: path) + } + public func manifests(at path: AbsolutePath) -> Set { manifestLoader.manifests(at: path) } diff --git a/Sources/TuistLoader/Loaders/ManifestLoader.swift b/Sources/TuistLoader/Loaders/ManifestLoader.swift index d1cfacf237c..3619620e81f 100644 --- a/Sources/TuistLoader/Loaders/ManifestLoader.swift +++ b/Sources/TuistLoader/Loaders/ManifestLoader.swift @@ -10,7 +10,7 @@ public enum ManifestLoaderError: FatalError, Equatable { case unexpectedOutput(AbsolutePath) case manifestNotFound(Manifest?, AbsolutePath) case manifestCachingFailed(Manifest?, AbsolutePath) - case manifestLoadingFailed(path: AbsolutePath, context: String) + case manifestLoadingFailed(path: AbsolutePath, data: Data, context: String) public static func manifestNotFound(_ path: AbsolutePath) -> ManifestLoaderError { .manifestNotFound(nil, path) @@ -26,7 +26,7 @@ public enum ManifestLoaderError: FatalError, Equatable { return "\(manifest?.fileName(path) ?? "Manifest") not found at path \(path.pathString)" case let .manifestCachingFailed(manifest, path): return "Could not cache \(manifest?.fileName(path) ?? "Manifest") at path \(path.pathString)" - case let .manifestLoadingFailed(path, context): + case let .manifestLoadingFailed(path, _, context): return """ Unable to load manifest at \(path.pathString.bold()) \(context) @@ -72,9 +72,13 @@ public protocol ManifestLoading { /// Loads the Dependencies.swift in the given directory /// - Parameters: - /// - path: Path to the directory that contains Dependencies.swift + /// - Parameter path: Path to the directory that contains the Package.swift func loadDependencies(at path: AbsolutePath) throws -> ProjectDescription.Dependencies + /// Loads the `PackageSettings` from `Package.swift` in the given directory + /// - path: Path to the directory that contains Dependencies.swift + func loadPackageSettings(at path: AbsolutePath) throws -> ProjectDescription.PackageSettings + /// Loads the Plugin.swift in the given directory. /// - Parameter path: Path to the directory that contains Plugin.swift func loadPlugin(at path: AbsolutePath) throws -> ProjectDescription.Plugin @@ -106,6 +110,8 @@ public class ManifestLoader: ManifestLoading { private var plugins: Plugins = .none private let cacheDirectoryProviderFactory: CacheDirectoriesProviderFactoring private let projectDescriptionHelpersBuilderFactory: ProjectDescriptionHelpersBuilderFactoring + private let xcodeController: XcodeControlling + private let swiftPackageManagerController: SwiftPackageManagerControlling // MARK: - Init @@ -115,7 +121,9 @@ public class ManifestLoader: ManifestLoading { resourceLocator: ResourceLocator(), cacheDirectoryProviderFactory: CacheDirectoriesProviderFactory(), projectDescriptionHelpersBuilderFactory: ProjectDescriptionHelpersBuilderFactory(), - manifestFilesLocator: ManifestFilesLocator() + manifestFilesLocator: ManifestFilesLocator(), + xcodeController: XcodeController.shared, + swiftPackageManagerController: SwiftPackageManagerController() ) } @@ -124,13 +132,17 @@ public class ManifestLoader: ManifestLoading { resourceLocator: ResourceLocating, cacheDirectoryProviderFactory: CacheDirectoriesProviderFactoring, projectDescriptionHelpersBuilderFactory: ProjectDescriptionHelpersBuilderFactoring, - manifestFilesLocator: ManifestFilesLocating + manifestFilesLocator: ManifestFilesLocating, + xcodeController: XcodeControlling, + swiftPackageManagerController: SwiftPackageManagerControlling ) { self.environment = environment self.resourceLocator = resourceLocator self.cacheDirectoryProviderFactory = cacheDirectoryProviderFactory self.projectDescriptionHelpersBuilderFactory = projectDescriptionHelpersBuilderFactory self.manifestFilesLocator = manifestFilesLocator + self.xcodeController = xcodeController + self.swiftPackageManagerController = swiftPackageManagerController decoder = JSONDecoder() } @@ -166,6 +178,24 @@ public class ManifestLoader: ManifestLoading { return try loadManifest(.dependencies, at: dependencyPath) } + public func loadPackageSettings(at path: AbsolutePath) throws -> ProjectDescription.PackageSettings { + let packageManifestPath = path.appending(components: Constants.tuistDirectoryName) + do { + return try loadManifest(.package, at: packageManifestPath) + } catch let error as ManifestLoaderError { + switch error { + case let .manifestLoadingFailed(path: _, data: data, context: _): + if data.count == 0 { + return PackageSettings() + } else { + throw error + } + default: + throw error + } + } + } + public func loadPlugin(at path: AbsolutePath) throws -> ProjectDescription.Plugin { try loadManifest(.plugin, at: path) } @@ -197,7 +227,9 @@ public class ManifestLoader: ManifestLoading { } catch { guard let error = error as? DecodingError else { throw ManifestLoaderError.manifestLoadingFailed( - path: manifestPath, context: error.localizedDescription + path: manifestPath, + data: data, + context: error.localizedDescription ) } @@ -207,6 +239,7 @@ public class ManifestLoader: ManifestLoading { case let .typeMismatch(type, context): throw ManifestLoaderError.manifestLoadingFailed( path: manifestPath, + data: data, context: """ The content of the manifest did not match the expected type of: \(String(describing: type).bold()) \(context.debugDescription) @@ -215,6 +248,7 @@ public class ManifestLoader: ManifestLoading { case let .valueNotFound(value, _): throw ManifestLoaderError.manifestLoadingFailed( path: manifestPath, + data: data, context: """ Expected a non-optional value for property of type \(String(describing: value).bold()) but found a nil value. \(json.bold()) @@ -223,6 +257,7 @@ public class ManifestLoader: ManifestLoading { case let .keyNotFound(codingKey, _): throw ManifestLoaderError.manifestLoadingFailed( path: manifestPath, + data: data, context: """ Did not find property with name \(codingKey.stringValue.bold()) in the JSON represented by: \(json.bold()) @@ -231,6 +266,7 @@ public class ManifestLoader: ManifestLoading { case let .dataCorrupted(context): throw ManifestLoaderError.manifestLoadingFailed( path: manifestPath, + data: data, context: """ The encoded data for the manifest is corrupted. \(context.debugDescription) @@ -239,6 +275,7 @@ public class ManifestLoader: ManifestLoading { @unknown default: throw ManifestLoaderError.manifestLoadingFailed( path: manifestPath, + data: data, context: """ Unable to decode the manifest for an unknown reason. \(error.localizedDescription) @@ -326,7 +363,8 @@ public class ManifestLoader: ManifestLoading { .dependencies, .project, .template, - .workspace: + .workspace, + .package: frameworkName = "ProjectDescription" } var arguments = [ @@ -350,7 +388,8 @@ public class ManifestLoader: ManifestLoading { case .dependencies, .project, .template, - .workspace: + .workspace, + .package: return try projectDescriptionHelpersBuilderFactory.projectDescriptionHelpersBuilder( cacheDirectory: projectDescriptionHelpersCacheDirectory ) @@ -367,7 +406,29 @@ public class ManifestLoader: ManifestLoading { } }() + let packageDescriptionArguments: [String] = try { + if case .package = manifest { + guard let xcode = try xcodeController.selected() else { return [] } + let packageVersion = try swiftPackageManagerController.getToolsVersion( + at: path.parentDirectory + ) + let manifestPath = + "\(xcode.path.pathString)/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/pm/ManifestAPI" + return [ + "-I", manifestPath, + "-L", manifestPath, + "-F", manifestPath, + "-lPackageDescription", + "-package-description-version", packageVersion.description, + "-D", "TUIST", + ] + } else { + return [] + } + }() + arguments.append(contentsOf: projectDescriptionHelperArguments) + arguments.append(contentsOf: packageDescriptionArguments) arguments.append(path.pathString) return arguments diff --git a/Sources/TuistLoader/Loaders/PackageSettingsLoader.swift b/Sources/TuistLoader/Loaders/PackageSettingsLoader.swift new file mode 100644 index 00000000000..2b733d0ea3c --- /dev/null +++ b/Sources/TuistLoader/Loaders/PackageSettingsLoader.swift @@ -0,0 +1,34 @@ +import Foundation +import ProjectDescription +import TSCBasic +import TuistCore +import TuistGraph +import TuistSupport + +/// Entity responsible for providing `PackageSettings`. +public protocol PackageSettingsLoading { + /// Load the Dependencies model at the specified path. + /// - Parameter path: The absolute path for the `PackageSettings` to load. + /// - Parameter plugins: The plugins for the `PackageSettings` to load. + /// - Returns: The `PackageSettings` loaded from the specified path. + func loadPackageSettings(at path: AbsolutePath, with plugins: Plugins) throws -> TuistGraph.PackageSettings +} + +public final class PackageSettingsLoader: PackageSettingsLoading { + private let manifestLoader: ManifestLoading + + public init(manifestLoader: ManifestLoading = ManifestLoader()) { + self.manifestLoader = manifestLoader + } + + public func loadPackageSettings(at path: AbsolutePath, with plugins: Plugins) throws -> TuistGraph.PackageSettings { + try manifestLoader.register(plugins: plugins) + let manifest = try manifestLoader.loadPackageSettings(at: path) + let generatorPaths = GeneratorPaths(manifestDirectory: path) + + return try TuistGraph.PackageSettings.from( + manifest: manifest, + generatorPaths: generatorPaths + ) + } +} diff --git a/Sources/TuistLoader/Loaders/RecursiveManifestLoader.swift b/Sources/TuistLoader/Loaders/RecursiveManifestLoader.swift index b660563b5bb..773484c72ce 100644 --- a/Sources/TuistLoader/Loaders/RecursiveManifestLoader.swift +++ b/Sources/TuistLoader/Loaders/RecursiveManifestLoader.swift @@ -79,7 +79,7 @@ public class RecursiveManifestLoader: RecursiveManifestLoading { return try manifestLoader.loadProject(at: $0, rootPath: rootPath) } var newDependenciesPaths = Set() - try zip(paths, projects).forEach { path, project in + for (path, project) in zip(paths, projects) { cache[path] = project newDependenciesPaths.formUnion(try dependencyPaths(for: project, path: path)) } diff --git a/Sources/TuistLoader/Log/Logger.swift b/Sources/TuistLoader/Log/Logger.swift index ffa409b2815..e6f04b3c7d5 100644 --- a/Sources/TuistLoader/Log/Logger.swift +++ b/Sources/TuistLoader/Log/Logger.swift @@ -1,2 +1,3 @@ import TuistSupport + let logger = Logger(label: "io.tuist.loader") diff --git a/Sources/TuistLoader/Models+ManifestMappers/Config+ManifestMapper.swift b/Sources/TuistLoader/Models+ManifestMappers/Config+ManifestMapper.swift index 6b2dc4356cc..efb8f0a0ec8 100644 --- a/Sources/TuistLoader/Models+ManifestMappers/Config+ManifestMapper.swift +++ b/Sources/TuistLoader/Models+ManifestMappers/Config+ManifestMapper.swift @@ -69,7 +69,8 @@ extension TuistGraph.Config.GenerationOptions { disablePackageVersionLocking: manifest.disablePackageVersionLocking, clonedSourcePackagesDirPath: clonedSourcePackagesDirPath, staticSideEffectsWarningTargets: TuistGraph.Config.GenerationOptions.StaticSideEffectsWarningTargets - .from(manifest: manifest.staticSideEffectsWarningTargets) + .from(manifest: manifest.staticSideEffectsWarningTargets), + enforceExplicitDependencies: manifest.enforceExplicitDependencies ) } } diff --git a/Sources/TuistLoader/Models+ManifestMappers/PackageSettings+ManifestMapper.swift b/Sources/TuistLoader/Models+ManifestMappers/PackageSettings+ManifestMapper.swift new file mode 100644 index 00000000000..8f0fe37b475 --- /dev/null +++ b/Sources/TuistLoader/Models+ManifestMappers/PackageSettings+ManifestMapper.swift @@ -0,0 +1,31 @@ +import Foundation +import ProjectDescription +import TSCBasic +import TuistCore +import TuistGraph +import TuistSupport + +extension TuistGraph.PackageSettings { + /// Creates `TuistGraph.PackageSettings` instance from `ProjectDescription.PackageSettings` + /// instance. + static func from( + manifest: ProjectDescription.PackageSettings, + generatorPaths: GeneratorPaths + ) throws -> Self { + let productTypes = manifest.productTypes.mapValues { TuistGraph.Product.from(manifest: $0) } + let baseSettings = try TuistGraph.Settings.from(manifest: manifest.baseSettings, generatorPaths: generatorPaths) + let targetSettings = manifest.targetSettings.mapValues { TuistGraph.SettingsDictionary.from(manifest: $0) } + let projectOptions: [String: TuistGraph.Project.Options] = manifest + .projectOptions + .mapValues { .from(manifest: $0) } + let platforms = try Set(manifest.platforms.map { try TuistGraph.PackagePlatform.from(manifest: $0) }) + + return .init( + productTypes: productTypes, + baseSettings: baseSettings, + targetSettings: targetSettings, + projectOptions: projectOptions, + platforms: platforms + ) + } +} diff --git a/Sources/TuistLoader/Models+ManifestMappers/ResourceFileElement+ManifestMapper.swift b/Sources/TuistLoader/Models+ManifestMappers/ResourceFileElement+ManifestMapper.swift index b6a25649982..58c678f5f77 100644 --- a/Sources/TuistLoader/Models+ManifestMappers/ResourceFileElement+ManifestMapper.swift +++ b/Sources/TuistLoader/Models+ManifestMappers/ResourceFileElement+ManifestMapper.swift @@ -18,7 +18,7 @@ extension TuistGraph.ResourceFileElement { ) throws -> [TuistGraph.ResourceFileElement] { func globFiles(_ path: AbsolutePath, excluding: [String]) throws -> [AbsolutePath] { var excluded: Set = [] - try excluding.forEach { path in + for path in excluding { let absolute = try AbsolutePath(validating: path) let globs = try AbsolutePath(validating: absolute.dirname).glob(absolute.basename) excluded.formUnion(globs) diff --git a/Sources/TuistLoader/Models+ManifestMappers/Target+ManifestMapper.swift b/Sources/TuistLoader/Models+ManifestMappers/Target+ManifestMapper.swift index e00ed2c7c4d..eb2db60ec73 100644 --- a/Sources/TuistLoader/Models+ManifestMappers/Target+ManifestMapper.swift +++ b/Sources/TuistLoader/Models+ManifestMappers/Target+ManifestMapper.swift @@ -161,20 +161,19 @@ extension TuistGraph.Target { } } - allResources - .forEach { fileElement in - switch fileElement { - case .folderReference: filteredResources.append(fileElement) - case let .file(path, _, _): - if path.extension == "playground" { - playgrounds.insert(path) - } else if path.extension == "xcdatamodeld" { - coreDataModels.insert(path) - } else { - filteredResources.append(fileElement) - } + for fileElement in allResources { + switch fileElement { + case .folderReference: filteredResources.append(fileElement) + case let .file(path, _, _): + if path.extension == "playground" { + playgrounds.insert(path) + } else if path.extension == "xcdatamodeld" { + coreDataModels.insert(path) + } else { + filteredResources.append(fileElement) } } + } return ( resources: filteredResources, @@ -206,7 +205,7 @@ extension TuistGraph.Target { ) } ?? []) - allSources.forEach { sourceFile in + for sourceFile in allSources { if sourceFile.path.extension == "playground" { playgrounds.insert(sourceFile.path) } else { diff --git a/Sources/TuistLoader/Models+ManifestMappers/TargetAction+ManifestMapper.swift b/Sources/TuistLoader/Models+ManifestMappers/TargetAction+ManifestMapper.swift index bd08cc00b5b..3f3419f29d9 100644 --- a/Sources/TuistLoader/Models+ManifestMappers/TargetAction+ManifestMapper.swift +++ b/Sources/TuistLoader/Models+ManifestMappers/TargetAction+ManifestMapper.swift @@ -15,9 +15,13 @@ extension TuistGraph.TargetScript { { let name = manifest.name let order = TuistGraph.TargetScript.Order.from(manifest: manifest.order) - let inputPaths = try manifest.inputPaths.compactMap { try $0.unfold(generatorPaths: generatorPaths) }.flatMap { $0 } + let inputPaths = try manifest.inputPaths + .compactMap { try $0.unfold(generatorPaths: generatorPaths) } + .flatMap { $0 } + .map(\.pathString) let inputFileListPaths = try absolutePaths(for: manifest.inputFileListPaths, generatorPaths: generatorPaths) let outputPaths = try absolutePaths(for: manifest.outputPaths, generatorPaths: generatorPaths) + .map(\.pathString) let outputFileListPaths = try absolutePaths(for: manifest.outputFileListPaths, generatorPaths: generatorPaths) let basedOnDependencyAnalysis = manifest.basedOnDependencyAnalysis let runForInstallBuildsOnly = manifest.runForInstallBuildsOnly diff --git a/Sources/TuistLoader/Models+ManifestMappers/TargetDependency+ManifestMapper.swift b/Sources/TuistLoader/Models+ManifestMappers/TargetDependency+ManifestMapper.swift index 88abdadf286..721680c67f1 100644 --- a/Sources/TuistLoader/Models+ManifestMappers/TargetDependency+ManifestMapper.swift +++ b/Sources/TuistLoader/Models+ManifestMappers/TargetDependency+ManifestMapper.swift @@ -26,7 +26,7 @@ extension TuistGraph.TargetDependency { /// - manifest: Manifest representation of the target dependency model. /// - generatorPaths: Generator paths. /// - externalDependencies: External dependencies graph. - static func from( + static func from( // swiftlint:disable:this function_body_length manifest: ProjectDescription.TargetDependency, generatorPaths: GeneratorPaths, externalDependencies: [String: [TuistGraph.TargetDependency]] diff --git a/Sources/TuistLoader/Models+ManifestMappers/TestAction+ManifestMapper.swift b/Sources/TuistLoader/Models+ManifestMappers/TestAction+ManifestMapper.swift index fe07fe4dfab..de61bd5f2c7 100644 --- a/Sources/TuistLoader/Models+ManifestMappers/TestAction+ManifestMapper.swift +++ b/Sources/TuistLoader/Models+ManifestMappers/TestAction+ManifestMapper.swift @@ -23,6 +23,7 @@ extension TuistGraph.TestAction { let language: SchemeLanguage? let region: String? let preferredScreenCaptureFormat: TuistGraph.ScreenCaptureFormat? + let skippedTests: [String]? if let plans = manifest.testPlans { testPlans = try plans.enumerated().compactMap { index, path in @@ -41,6 +42,7 @@ extension TuistGraph.TestAction { language = nil region = nil preferredScreenCaptureFormat = nil + skippedTests = nil } else { targets = try manifest.targets .map { try TuistGraph.TestableTarget.from(manifest: $0, generatorPaths: generatorPaths) } @@ -67,6 +69,7 @@ extension TuistGraph.TestAction { // not used when using targets testPlans = nil + skippedTests = manifest.skippedTests } let configurationName = manifest.configuration.rawValue @@ -93,7 +96,8 @@ extension TuistGraph.TestAction { language: language?.identifier, region: region, preferredScreenCaptureFormat: preferredScreenCaptureFormat, - testPlans: testPlans + testPlans: testPlans, + skippedTests: skippedTests ) } } diff --git a/Sources/TuistLoader/Models/FileListGlob+Unfold.swift b/Sources/TuistLoader/Models/FileListGlob+Unfold.swift index 8c9903468ce..46663d31d96 100644 --- a/Sources/TuistLoader/Models/FileListGlob+Unfold.swift +++ b/Sources/TuistLoader/Models/FileListGlob+Unfold.swift @@ -31,7 +31,7 @@ extension FileListGlob { private func resolvedExcluding(generatorPaths: GeneratorPaths) throws -> Set { guard !excluding.isEmpty else { return [] } var result: Set = [] - try excluding.forEach { path in + for path in excluding { let resolved = try generatorPaths.resolve(path: path) let globs = try AbsolutePath(validating: resolved.dirname).glob(resolved.basename) result.formUnion(globs) diff --git a/Sources/TuistLoader/Models/Manifest.swift b/Sources/TuistLoader/Models/Manifest.swift index bfecde57b28..23a999a12b1 100644 --- a/Sources/TuistLoader/Models/Manifest.swift +++ b/Sources/TuistLoader/Models/Manifest.swift @@ -8,6 +8,7 @@ public enum Manifest: CaseIterable { case template case dependencies case plugin + case package /// - Parameters: /// - path: Path to the folder that contains the manifest @@ -26,6 +27,8 @@ public enum Manifest: CaseIterable { return "Dependencies.swift" case .plugin: return "Plugin.swift" + case .package: + return "Package.swift" } } } diff --git a/Sources/TuistLoader/Utils/ManifestFilesLocator.swift b/Sources/TuistLoader/Utils/ManifestFilesLocator.swift index 2f028980356..c64243eff73 100644 --- a/Sources/TuistLoader/Utils/ManifestFilesLocator.swift +++ b/Sources/TuistLoader/Utils/ManifestFilesLocator.swift @@ -37,6 +37,12 @@ public protocol ManifestFilesLocating: AnyObject { /// It traverses up the directory hierarchy until it finds a `Dependencies.swift` file. /// - Parameter locatingPath: Path from where to do the lookup. func locateDependencies(at locatingPath: AbsolutePath) -> AbsolutePath? + + /// It traverses up the directory hierarchy until it finds a `Package.swift` file + /// - Parameter locatingPath: Path from where to do the lookup + func locatePackageManifest( + at locatingPath: AbsolutePath + ) -> AbsolutePath? } public final class ManifestFilesLocator: ManifestFilesLocating { @@ -185,6 +191,13 @@ public final class ManifestFilesLocator: ManifestFilesLocating { return traverseAndLocate(at: locatingPath, appending: subPath) } + public func locatePackageManifest(at locatingPath: AbsolutePath) -> AbsolutePath? { + let subPath = + // swiftlint:disable:next force_try + try! RelativePath(validating: "\(Constants.tuistDirectoryName)/Package.swift") + return traverseAndLocate(at: locatingPath, appending: subPath) + } + // MARK: - Helpers private func traverseAndLocate(at locatingPath: AbsolutePath, appending subpath: RelativePath) -> AbsolutePath? { diff --git a/Sources/TuistLoaderTesting/Loaders/Mocks/MockManifestLoader.swift b/Sources/TuistLoaderTesting/Loaders/Mocks/MockManifestLoader.swift index e2aa63b56e4..46d349feed1 100644 --- a/Sources/TuistLoaderTesting/Loaders/Mocks/MockManifestLoader.swift +++ b/Sources/TuistLoaderTesting/Loaders/Mocks/MockManifestLoader.swift @@ -28,6 +28,9 @@ public final class MockManifestLoader: ManifestLoading { public var loadDependenciesCount: UInt = 0 public var loadDependenciesStub: ((AbsolutePath) throws -> Dependencies)? + public var loadPackageSettingsCount: UInt = 0 + public var loadPackageSettingsStub: ((AbsolutePath) throws -> PackageSettings)? + public var loadPluginCount: UInt = 0 public var loadPluginStub: ((AbsolutePath) throws -> Plugin)? @@ -73,6 +76,11 @@ public final class MockManifestLoader: ManifestLoading { return try loadDependenciesStub?(path) ?? Dependencies.test() } + public func loadPackageSettings(at path: AbsolutePath) throws -> PackageSettings { + loadPackageSettingsCount += 1 + return try loadPackageSettingsStub?(path) ?? .test() + } + public func loadPlugin(at path: AbsolutePath) throws -> Plugin { loadPluginCount += 1 return try loadPluginStub?(path) ?? Plugin.test() diff --git a/Sources/TuistLoaderTesting/Loaders/Mocks/MockPackageSettingsLoader.swift b/Sources/TuistLoaderTesting/Loaders/Mocks/MockPackageSettingsLoader.swift new file mode 100644 index 00000000000..b065c46c515 --- /dev/null +++ b/Sources/TuistLoaderTesting/Loaders/Mocks/MockPackageSettingsLoader.swift @@ -0,0 +1,25 @@ +import TSCBasic +import TuistGraph +import TuistGraphTesting +import TuistSupport + +@testable import TuistLoader + +public class MockPackageSettingsLoader: PackageSettingsLoading { + public init() {} + + public var invokedLoadPackageSettings = false + public var invokedLoadPackageSettingsCount = 0 + public var invokedLoadPackageSettingsParameters: (AbsolutePath, Plugins)? + public var invokedLoadPackageSettingsParemetersList = [(AbsolutePath, Plugins)]() + public var loadPackageSettingsStub: ((AbsolutePath, Plugins) throws -> PackageSettings)? + + public func loadPackageSettings(at path: AbsolutePath, with plugins: Plugins) throws -> PackageSettings { + invokedLoadPackageSettings = true + invokedLoadPackageSettingsCount += 1 + invokedLoadPackageSettingsParameters = (path, plugins) + invokedLoadPackageSettingsParemetersList.append((path, plugins)) + + return try loadPackageSettingsStub?(path, plugins) ?? PackageSettings.test() + } +} diff --git a/Sources/TuistLoaderTesting/Loaders/TestData/ProjectDescription+TestData.swift b/Sources/TuistLoaderTesting/Loaders/TestData/ProjectDescription+TestData.swift index 1f9cc077ac8..fe138385ff4 100644 --- a/Sources/TuistLoaderTesting/Loaders/TestData/ProjectDescription+TestData.swift +++ b/Sources/TuistLoaderTesting/Loaders/TestData/ProjectDescription+TestData.swift @@ -218,3 +218,9 @@ extension Plugin { Plugin(name: name) } } + +extension PackageSettings { + public static func test() -> PackageSettings { + PackageSettings() + } +} diff --git a/Sources/TuistLoaderTesting/Utils/Mocks/MockManifestFilesLocator.swift b/Sources/TuistLoaderTesting/Utils/Mocks/MockManifestFilesLocator.swift index 418721d3729..e99ec97d4fd 100644 --- a/Sources/TuistLoaderTesting/Utils/Mocks/MockManifestFilesLocator.swift +++ b/Sources/TuistLoaderTesting/Utils/Mocks/MockManifestFilesLocator.swift @@ -12,6 +12,8 @@ public final class MockManifestFilesLocator: ManifestFilesLocating { public var locateConfigArgs: [AbsolutePath] = [] public var locateDependenciesStub: AbsolutePath? public var locateDependenciesArgs: [AbsolutePath] = [] + public var locatePackageManifestStub: AbsolutePath? + public var locatePackageManifestArgs: [AbsolutePath] = [] public init() {} @@ -51,4 +53,9 @@ public final class MockManifestFilesLocator: ManifestFilesLocating { locateDependenciesArgs.append(at) return locateDependenciesStub ?? at.appending(components: "Tuist", "Dependencies.swift") } + + public func locatePackageManifest(at: AbsolutePath) -> AbsolutePath? { + locatePackageManifestArgs.append(at) + return locatePackageManifestStub ?? at.appending(components: "Tuist", "Package.swift") + } } diff --git a/Sources/TuistMigration/Log/Logger.swift b/Sources/TuistMigration/Log/Logger.swift index 6b7feff25cc..862e66ab1ed 100644 --- a/Sources/TuistMigration/Log/Logger.swift +++ b/Sources/TuistMigration/Log/Logger.swift @@ -1,2 +1,3 @@ import TuistSupport + let logger = Logger(label: "io.tuist.migration") diff --git a/Sources/TuistMigration/Utilities/EmptyBuildSettingsChecker.swift b/Sources/TuistMigration/Utilities/EmptyBuildSettingsChecker.swift index 2979e5a88c8..9222639d991 100644 --- a/Sources/TuistMigration/Utilities/EmptyBuildSettingsChecker.swift +++ b/Sources/TuistMigration/Utilities/EmptyBuildSettingsChecker.swift @@ -57,7 +57,7 @@ public class EmptyBuildSettingsChecker: EmptyBuildSettingsChecking { let buildConfigurations = try buildConfigurations(pbxproj: pbxproj, targetName: targetName) let nonEmptyBuildSettings = buildConfigurations.compactMap { config -> String? in if config.buildSettings.isEmpty { return nil } - config.buildSettings.forEach { key, _ in + for (key, _) in config.buildSettings { logger.info("The build setting '\(key)' of build configuration '\(config.name)' is not empty.") } return config.name diff --git a/Sources/TuistMigration/Utilities/SettingsToXCConfigExtractor.swift b/Sources/TuistMigration/Utilities/SettingsToXCConfigExtractor.swift index 4f350fd278e..d1ae3db8c7a 100644 --- a/Sources/TuistMigration/Utilities/SettingsToXCConfigExtractor.swift +++ b/Sources/TuistMigration/Utilities/SettingsToXCConfigExtractor.swift @@ -71,13 +71,13 @@ public class SettingsToXCConfigExtractor: SettingsToXCConfigExtracting { var buildSettingsLines: [String] = [] // Common build settings - commonBuildSettings.forEach { setting in + for setting in commonBuildSettings { let value = buildConfigurations.first!.buildSettings[setting]! commonBuildSettingsLines.append("\(setting)=\(flattenedValue(from: value))") } // Per-configuration build settings - buildConfigurations.forEach { configuration in + for configuration in buildConfigurations { configuration.buildSettings.forEach { key, value in if commonBuildSettings.contains(key) { return } buildSettingsLines.append("\(key)[config=\(configuration.name)]=\(flattenedValue(from: value))") diff --git a/Sources/TuistPlugin/Logger.swift b/Sources/TuistPlugin/Logger.swift index 5cb50090b54..b96cfdec0ef 100644 --- a/Sources/TuistPlugin/Logger.swift +++ b/Sources/TuistPlugin/Logger.swift @@ -1,2 +1,3 @@ import TuistSupport + let logger = Logger(label: "io.tuist.plugin") diff --git a/Sources/TuistPlugin/PluginService.swift b/Sources/TuistPlugin/PluginService.swift index afdadd44d97..662e9d92079 100644 --- a/Sources/TuistPlugin/PluginService.swift +++ b/Sources/TuistPlugin/PluginService.swift @@ -308,10 +308,10 @@ public final class PluginService: PluginServicing { try? fileUnarchiver.delete() } try FileHandler.shared.createFolder(pluginReleaseDirectory) - try unarchivedContents.forEach { + for unarchivedContent in unarchivedContents { try FileHandler.shared.move( - from: $0, - to: pluginReleaseDirectory.appending(component: $0.basename) + from: unarchivedContent, + to: pluginReleaseDirectory.appending(component: unarchivedContent.basename) ) } diff --git a/Sources/TuistScaffold/TemplateGenerator.swift b/Sources/TuistScaffold/TemplateGenerator.swift index a23b236b955..0d923c75bcb 100644 --- a/Sources/TuistScaffold/TemplateGenerator.swift +++ b/Sources/TuistScaffold/TemplateGenerator.swift @@ -107,9 +107,9 @@ public final class TemplateGenerator: TemplateGenerating { destinationPath: AbsolutePath ) throws { let environment = stencilSwiftEnvironment() - try renderedItems.forEach { + for renderedItem in renderedItems { let renderedContents: String? - switch $0.contents { + switch renderedItem.contents { case let .string(contents): renderedContents = try environment.renderTemplate( string: contents, @@ -129,7 +129,7 @@ public final class TemplateGenerator: TemplateGenerating { } case let .directory(path): let destinationDirectoryPath = destinationPath - .appending(try RelativePath(validating: $0.path.pathString)) + .appending(try RelativePath(validating: renderedItem.path.pathString)) .appending(component: path.basename) // workaround for creating folder tree of destinationDirectoryPath if !FileHandler.shared.exists(destinationDirectoryPath.parentDirectory) { @@ -144,11 +144,11 @@ public final class TemplateGenerator: TemplateGenerating { // Generate file only when it has some content, unless it is a `.gitkeep` file if let rendered = renderedContents, !rendered.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty || - $0.path.basename == ".gitkeep" + renderedItem.path.basename == ".gitkeep" { try FileHandler.shared.write( rendered, - path: destinationPath.appending($0.path), + path: destinationPath.appending(renderedItem.path), atomically: true ) } diff --git a/Sources/TuistSigning/Certificate/CertificateParser.swift b/Sources/TuistSigning/Certificate/CertificateParser.swift index 5ecd6df7c48..0307bb02e5b 100644 --- a/Sources/TuistSigning/Certificate/CertificateParser.swift +++ b/Sources/TuistSigning/Certificate/CertificateParser.swift @@ -134,13 +134,13 @@ extension String { let matches = regex.matches(in: self, options: [], range: NSRange(startIndex..., in: self)).reversed() var modifiableString = self - matches.forEach { result in + for result in matches { guard let firstRange = Range(result.range(at: 2), in: modifiableString), let secondRange = Range(result.range(at: 4), in: modifiableString), let firstInt = UInt8(modifiableString[firstRange], radix: 16), let secondInt = UInt8(modifiableString[secondRange], radix: 16) else { - return + continue } let resultRange = Range(result.range, in: modifiableString)! modifiableString.replaceSubrange( diff --git a/Sources/TuistSigning/Log/Logger.swift b/Sources/TuistSigning/Log/Logger.swift index c05fe76bd4d..03bcaf3a40b 100644 --- a/Sources/TuistSigning/Log/Logger.swift +++ b/Sources/TuistSigning/Log/Logger.swift @@ -1,2 +1,3 @@ import TuistSupport + let logger = Logger(label: "io.tuist.signing") diff --git a/Sources/TuistSigning/SigningCipher.swift b/Sources/TuistSigning/SigningCipher.swift index 1e8fed5cb9e..c6752bfa7f4 100644 --- a/Sources/TuistSigning/SigningCipher.swift +++ b/Sources/TuistSigning/SigningCipher.swift @@ -84,12 +84,11 @@ public final class SigningCipher: SigningCiphering { .map(FileHandler.shared.readFile) .map { try encryptData($0, masterKey: masterKey) } - try zip(cipheredKeys, signingKeyFiles) - .forEach { key, file in - logger.debug("Encrypting \(file.pathString)") - let encryptedPath = try AbsolutePath(validating: file.pathString + "." + Constants.encryptedExtension) - try key.write(to: encryptedPath.url) - } + for (key, file) in zip(cipheredKeys, signingKeyFiles) { + logger.debug("Encrypting \(file.pathString)") + let encryptedPath = try AbsolutePath(validating: file.pathString + "." + Constants.encryptedExtension) + try key.write(to: encryptedPath.url) + } if !keepFiles { try signingKeyFiles.forEach(FileHandler.shared.delete) @@ -109,7 +108,7 @@ public final class SigningCipher: SigningCiphering { try locateUnencryptedSigningFiles(at: path) .forEach(FileHandler.shared.delete) - try zip(decipheredKeys, signingKeyFiles).forEach { key, keyFile in + for (key, keyFile) in zip(decipheredKeys, signingKeyFiles) { logger.debug("Decrypting \(keyFile.pathString)") let decryptedPath = try AbsolutePath( validating: keyFile.parentDirectory.pathString + "/" + keyFile diff --git a/Sources/TuistSigning/SigningInteractor.swift b/Sources/TuistSigning/SigningInteractor.swift index 3d3ada2c808..4b48251951e 100644 --- a/Sources/TuistSigning/SigningInteractor.swift +++ b/Sources/TuistSigning/SigningInteractor.swift @@ -109,8 +109,8 @@ public final class SigningInteractor: SigningInteracting { return (certificate: certificate, provisioningProfile: provisioningProfile) } - try signingPairs.map(\.certificate).forEach { - try signingInstaller.installCertificate($0, keychainPath: keychainPath) + for signingPair in signingPairs.map(\.certificate) { + try signingInstaller.installCertificate(signingPair, keychainPath: keychainPath) } let provisioningProfileInstallLintIssues = try signingPairs.map(\.provisioningProfile) diff --git a/Sources/TuistSupport/Constants.swift b/Sources/TuistSupport/Constants.swift index 87ac2794b83..fa14b81692f 100644 --- a/Sources/TuistSupport/Constants.swift +++ b/Sources/TuistSupport/Constants.swift @@ -8,7 +8,7 @@ public enum Constants { public static let githubAPIURL = "https://api.github.com" public static let githubSlug = "tuist/tuist" public static let communityURL = "https://github.com/tuist/tuist/discussions/categories/general" - public static let version = "3.35.6-runtastic" + public static let version = "3.42.2.1-runtastic" public static let bundleName: String = "tuist.zip" public static let envBundleName: String = "tuistenv.zip" public static let trueValues: [String] = ["1", "true", "TRUE", "yes", "YES"] @@ -26,7 +26,6 @@ public enum Constants { public static let twitterHandle: String = "tuistio" public static let joinSlackURL: String = "https://slack.tuist.io/" public static let tuistGeneratedFileName = ".tuist-generated" - public static let tuistCloudURL: String = "https://cloud.tuist.io/" /// The cache version. /// This should change only when it changes the logic to map a `TuistGraph.Target` to a cached build artifact. @@ -70,7 +69,6 @@ public enum Constants { public static let forceConfigCacheDirectory = "TUIST_CONFIG_FORCE_CONFIG_CACHE_DIRECTORY" public static let automationPath = "TUIST_CONFIG_AUTOMATION_PATH" public static let queueDirectory = "TUIST_CONFIG_QUEUE_DIRECTORY" - public static let cloudToken = "TUIST_CONFIG_CLOUD_TOKEN" public static let cacheManifests = "TUIST_CONFIG_CACHE_MANIFESTS" public static let statsOptOut = "TUIST_CONFIG_STATS_OPT_OUT" public static let githubAPIToken = "TUIST_CONFIG_GITHUB_API_TOKEN" @@ -78,12 +76,5 @@ public enum Constants { public static let osLog = "TUIST_CONFIG_OS_LOG" /// `tuistBinaryPath` is used for specifying the exact tuist binary in tuist tasks. public static let tuistBinaryPath = "TUIST_CONFIG_BINARY_PATH" - /// Default URL for Tuist Cloud - public static let cloudURL = "TUIST_CLOUD_URL" - } - - public enum AutogeneratedScheme { - public static let binariesSchemeNamePrefix: String = "ProjectCache-Binaries" - public static let bundlesSchemeNamePrefix: String = "ProjectCache-Bundles" } } diff --git a/Sources/TuistSupport/Credentials/CredentialsStore.swift b/Sources/TuistSupport/Credentials/CredentialsStore.swift index 921353c4959..0887e4b168d 100644 --- a/Sources/TuistSupport/Credentials/CredentialsStore.swift +++ b/Sources/TuistSupport/Credentials/CredentialsStore.swift @@ -70,7 +70,7 @@ public final class CredentialsStore: CredentialsStoring { public func delete(serverURL: URL) throws { let keychain = keychain(serverURL: serverURL) - try keychain.allKeys().forEach { account in + for account in keychain.allKeys() { try keychain.remove(account) } } diff --git a/Sources/TuistSupport/Extensions/String+MD5.swift b/Sources/TuistSupport/Extensions/String+MD5.swift index 68f0f56b757..2770736af2c 100644 --- a/Sources/TuistSupport/Extensions/String+MD5.swift +++ b/Sources/TuistSupport/Extensions/String+MD5.swift @@ -267,8 +267,8 @@ class MD5: HashProtocol { var result = [UInt8]() result.reserveCapacity(hh.count / 4) - hh.forEach { - let itemLE = $0.littleEndian + for item in hh { + let itemLE = item.littleEndian let r1 = UInt8(itemLE & 0xFF) let r2 = UInt8((itemLE >> 8) & 0xFF) let r3 = UInt8((itemLE >> 16) & 0xFF) diff --git a/Sources/TuistSupport/Logging/FileLogger.swift b/Sources/TuistSupport/Logging/FileLogger.swift new file mode 100644 index 00000000000..03c83106ff4 --- /dev/null +++ b/Sources/TuistSupport/Logging/FileLogger.swift @@ -0,0 +1,31 @@ +import Foundation +import TSCBasic + +public struct FileLogger: TextOutputStream { + enum FileHandlerOutputStream: Error { + case couldNotCreateFile + } + + private let fileHandle: FileHandle + let encoding: String.Encoding + + public init(path: AbsolutePath, encoding: String.Encoding = .utf8) throws { + if !FileHandler.shared.exists(path) { + if !FileHandler.shared.exists(path.parentDirectory) { + try FileHandler.shared.createFolder(path.parentDirectory) + } + try FileHandler.shared.touch(path) + } + + let fileHandle = try FileHandle(forWritingTo: path.url) + fileHandle.seekToEndOfFile() + self.fileHandle = fileHandle + self.encoding = encoding + } + + public func write(_ string: String) { + if let data = string.data(using: encoding) { + fileHandle.write(data) + } + } +} diff --git a/Sources/TuistSupport/SwiftPackageManager/PackageInfo.swift b/Sources/TuistSupport/SwiftPackageManager/PackageInfo.swift index 9c45a85460d..2f1314910d6 100644 --- a/Sources/TuistSupport/SwiftPackageManager/PackageInfo.swift +++ b/Sources/TuistSupport/SwiftPackageManager/PackageInfo.swift @@ -259,7 +259,12 @@ extension PackageInfo.Target { case target(name: String, condition: PackageInfo.PackageConditionDescription?) /// A product from an external package. - case product(name: String, package: String, condition: PackageInfo.PackageConditionDescription?) + case product( + name: String, + package: String, + moduleAliases: [String: String]?, + condition: PackageInfo.PackageConditionDescription? + ) /// A dependency to be resolved by name. case byName(name: String, condition: PackageInfo.PackageConditionDescription?) @@ -501,6 +506,7 @@ extension PackageInfo.Target.Dependency: Decodable { self = .product( name: first, package: try unkeyedValues.decodeIfPresent(String.self) ?? first, + moduleAliases: try unkeyedValues.decodeIfPresent([String: String].self), condition: try unkeyedValues.decodeIfPresent(PackageInfo.PackageConditionDescription.self) ) case .byName: diff --git a/Sources/TuistSupport/SwiftPackageManager/SwiftPackageManagerController.swift b/Sources/TuistSupport/SwiftPackageManager/SwiftPackageManagerController.swift index 4a8b6e226bb..b5558d73aab 100644 --- a/Sources/TuistSupport/SwiftPackageManager/SwiftPackageManagerController.swift +++ b/Sources/TuistSupport/SwiftPackageManager/SwiftPackageManagerController.swift @@ -16,6 +16,11 @@ public protocol SwiftPackageManagerControlling { /// - printOutput: When true it prints the Swift Package Manager's output. func update(at path: AbsolutePath, printOutput: Bool) throws + /// Gets the tools version of the package at the given path + /// - Parameter path: Directory where the `Package.swift` is defined. + /// - Returns: Version of tools. + func getToolsVersion(at path: AbsolutePath) throws -> Version + /// Sets tools version of package to the given value. /// - Parameter path: Directory where the `Package.swift` is defined. /// - Parameter version: Version of tools. When `nil` then the environment’s version will be set. @@ -66,6 +71,15 @@ public final class SwiftPackageManagerController: SwiftPackageManagerControlling try System.shared.run(command) } + public func getToolsVersion(at path: AbsolutePath) throws -> Version { + let extraArguments = ["tools-version"] + + let command = buildSwiftPackageCommand(packagePath: path, extraArguments: extraArguments) + + let rawVersion = try System.shared.capture(command).trimmingCharacters(in: .whitespacesAndNewlines) + return try Version(versionString: rawVersion) + } + public func loadPackageInfo(at path: AbsolutePath) throws -> PackageInfo { let command = buildSwiftPackageCommand(packagePath: path, extraArguments: ["dump-package"]) diff --git a/Sources/TuistSupport/Utils/Environment.swift b/Sources/TuistSupport/Utils/Environment.swift index 3725be0e7aa..e33e554194a 100644 --- a/Sources/TuistSupport/Utils/Environment.swift +++ b/Sources/TuistSupport/Utils/Environment.swift @@ -82,9 +82,9 @@ public class Environment: Environmenting { /// Sets up the local environment. public func bootstrap() throws { - try [directory, versionsDirectory].forEach { - if !fileHandler.exists($0) { - try fileHandler.createFolder($0) + for item in [directory, versionsDirectory] { + if !fileHandler.exists(item) { + try fileHandler.createFolder(item) } } } diff --git a/Sources/TuistSupport/Utils/FileArchiver.swift b/Sources/TuistSupport/Utils/FileArchiver.swift index 6490620d273..894a4b1537a 100644 --- a/Sources/TuistSupport/Utils/FileArchiver.swift +++ b/Sources/TuistSupport/Utils/FileArchiver.swift @@ -30,8 +30,8 @@ public class FileArchiver: FileArchiving { /// ZIPFoundation does not support zipping array of items, we instead copy them all to a single directory let pathsDirectoryPath = temporaryDirectory.appending(component: "\(name)-paths") try FileHandler.shared.createFolder(pathsDirectoryPath) - try paths.forEach { - try FileHandler.shared.copy(from: $0, to: pathsDirectoryPath.appending(component: $0.basename)) + for path in paths { + try FileHandler.shared.copy(from: path, to: pathsDirectoryPath.appending(component: path.basename)) } try FileHandler.shared.zipItem(at: pathsDirectoryPath, to: destinationZipPath) return destinationZipPath diff --git a/Sources/TuistSupport/Utils/Stack.swift b/Sources/TuistSupport/Utils/Stack.swift index 3af3db0faa9..73a2fe4d730 100644 --- a/Sources/TuistSupport/Utils/Stack.swift +++ b/Sources/TuistSupport/Utils/Stack.swift @@ -16,6 +16,10 @@ public struct Stack { array.append(element) } + public mutating func push(_ elements: [T]) { + array.append(contentsOf: elements) + } + public mutating func pop() -> T? { array.popLast() } diff --git a/Sources/TuistSupport/Utils/TextTable.swift b/Sources/TuistSupport/Utils/TextTable.swift index 1fab7deb7ee..c71e9331451 100644 --- a/Sources/TuistSupport/Utils/TextTable.swift +++ b/Sources/TuistSupport/Utils/TextTable.swift @@ -56,8 +56,8 @@ public struct TextTable { render(separator: separator, in: &table, widths: widths) // Data Rows - data.forEach { - let row = mapper($0) + for data in data { + let row = mapper(data) render(row: row, in: &table, widths: widths) } diff --git a/Sources/TuistSupport/Xcode/XcodeController.swift b/Sources/TuistSupport/Xcode/XcodeController.swift index 9c88af5777e..d739d1f141a 100644 --- a/Sources/TuistSupport/Xcode/XcodeController.swift +++ b/Sources/TuistSupport/Xcode/XcodeController.swift @@ -18,6 +18,8 @@ public protocol XcodeControlling { } public class XcodeController: XcodeControlling { + public init() {} + /// Shared instance. public static var shared: XcodeControlling = XcodeController() diff --git a/Sources/TuistSupportTesting/Extensions/XCTestCase+Extras.swift b/Sources/TuistSupportTesting/Extensions/XCTestCase+Extras.swift index 68f3c384ec7..24917b844a7 100644 --- a/Sources/TuistSupportTesting/Extensions/XCTestCase+Extras.swift +++ b/Sources/TuistSupportTesting/Extensions/XCTestCase+Extras.swift @@ -30,11 +30,23 @@ extension XCTestCase { // swiftlint:disable:next large_tuple public func XCTAssertEqualPairs(_ subjects: [(T, T, Bool)], file: StaticString = #file, line: UInt = #line) { - subjects.forEach { - if $0.2 { - XCTAssertEqual($0.0, $0.1, "Expected \($0.0) to be equal to \($0.1) but they are not.", file: file, line: line) + for subject in subjects { + if subject.2 { + XCTAssertEqual( + subject.0, + subject.1, + "Expected \(subject.0) to be equal to \(subject.1) but they are not.", + file: file, + line: line + ) } else { - XCTAssertNotEqual($0.0, $0.1, "Expected \($0.0) to not be equal to \($0.1) but they are.", file: file, line: line) + XCTAssertNotEqual( + subject.0, + subject.1, + "Expected \(subject.0) to not be equal to \(subject.1) but they are.", + file: file, + line: line + ) } } } @@ -66,6 +78,22 @@ extension XCTestCase { XCTAssertTrue(standardOutput.contains(pattern), message, file: file, line: line) } + public func XCTAssertStandardError(pattern: String, file: StaticString = #file, line: UInt = #line) { + let standardError = TestingLogHandler.collected[.error, ==] + + let message = """ + The standard error: + =========== + \(standardError) + + Doesn't contain the expected output: + =========== + \(pattern) + """ + + XCTAssertTrue(standardError.contains(pattern), message, file: file, line: line) + } + public func XCTTry(_ closure: @autoclosure @escaping () throws -> T, file: StaticString = #file, line: UInt = #line) -> T { var value: T! do { diff --git a/Sources/TuistSupportTesting/SwiftPackageManager/MockSwiftPackageManagerController.swift b/Sources/TuistSupportTesting/SwiftPackageManager/MockSwiftPackageManagerController.swift index 9561d954801..fc59a7c3060 100644 --- a/Sources/TuistSupportTesting/SwiftPackageManager/MockSwiftPackageManagerController.swift +++ b/Sources/TuistSupportTesting/SwiftPackageManager/MockSwiftPackageManagerController.swift @@ -26,6 +26,13 @@ public final class MockSwiftPackageManagerController: SwiftPackageManagerControl try setToolsVersionStub?(path, version) } + public var invokedGetToolsVersion = false + public var getToolsVersionStub: ((AbsolutePath) throws -> Version)? + public func getToolsVersion(at path: AbsolutePath) throws -> Version { + invokedGetToolsVersion = true + return try getToolsVersionStub?(path) ?? Version("5.4.0") + } + public var invokedLoadPackageInfo = false public var loadPackageInfoStub: ((AbsolutePath) throws -> PackageInfo)? public func loadPackageInfo(at path: AbsolutePath) throws -> PackageInfo { diff --git a/Sources/TuistSupportTesting/SwiftPackageManager/PackageInfo+TestData.swift b/Sources/TuistSupportTesting/SwiftPackageManager/PackageInfo+TestData.swift index f147cd3db1f..6e54d165da5 100644 --- a/Sources/TuistSupportTesting/SwiftPackageManager/PackageInfo+TestData.swift +++ b/Sources/TuistSupportTesting/SwiftPackageManager/PackageInfo+TestData.swift @@ -111,6 +111,7 @@ extension PackageInfo { "product" : [ "ALibrary", "a-dependency", + null, { "platformNames" : [ "ios" @@ -225,6 +226,7 @@ extension PackageInfo { "product" : [ "AnotherLibrary", "another-dependency", + null, null ] } @@ -378,6 +380,7 @@ extension PackageInfo { "product" : [ "ALibrary", "a-dependency", + null, { "platformNames" : [ "ios" @@ -504,6 +507,7 @@ extension PackageInfo { "product" : [ "AnotherLibrary", "another-dependency", + null, null ] } @@ -594,6 +598,7 @@ extension PackageInfo { .product( name: "ALibrary", package: "a-dependency", + moduleAliases: nil, condition: .init(platformNames: ["ios"], config: nil) ), ], @@ -636,7 +641,7 @@ extension PackageInfo { resources: [], exclude: [], dependencies: [ - .product(name: "AnotherLibrary", package: "another-dependency", condition: nil), + .product(name: "AnotherLibrary", package: "another-dependency", moduleAliases: nil, condition: nil), ], publicHeadersPath: nil, type: .regular, @@ -1033,6 +1038,7 @@ extension PackageInfo { "product" : [ "GULAppDelegateSwizzler", "GoogleUtilities", + null, null ] }, @@ -1040,6 +1046,7 @@ extension PackageInfo { "product" : [ "GULMethodSwizzler", "GoogleUtilities", + null, null ] }, @@ -1047,6 +1054,7 @@ extension PackageInfo { "product" : [ "GULNSData", "GoogleUtilities", + null, null ] }, @@ -1054,6 +1062,7 @@ extension PackageInfo { "product" : [ "GULNetwork", "GoogleUtilities", + null, null ] }, @@ -1061,6 +1070,7 @@ extension PackageInfo { "product" : [ "nanopb", "nanopb", + null, null ] } @@ -1135,6 +1145,7 @@ extension PackageInfo { "product" : [ "GULAppDelegateSwizzler", "GoogleUtilities", + null, null ] }, @@ -1142,6 +1153,7 @@ extension PackageInfo { "product" : [ "GULMethodSwizzler", "GoogleUtilities", + null, null ] }, @@ -1149,6 +1161,7 @@ extension PackageInfo { "product" : [ "GULNSData", "GoogleUtilities", + null, null ] }, @@ -1156,6 +1169,7 @@ extension PackageInfo { "product" : [ "GULNetwork", "GoogleUtilities", + null, null ] }, @@ -1163,6 +1177,7 @@ extension PackageInfo { "product" : [ "nanopb", "nanopb", + null, null ] } @@ -1258,11 +1273,11 @@ extension PackageInfo { exclude: [], dependencies: [ .byName(name: "GoogleAppMeasurement", condition: nil), - .product(name: "GULAppDelegateSwizzler", package: "GoogleUtilities", condition: nil), - .product(name: "GULMethodSwizzler", package: "GoogleUtilities", condition: nil), - .product(name: "GULNSData", package: "GoogleUtilities", condition: nil), - .product(name: "GULNetwork", package: "GoogleUtilities", condition: nil), - .product(name: "nanopb", package: "nanopb", condition: nil), + .product(name: "GULAppDelegateSwizzler", package: "GoogleUtilities", moduleAliases: nil, condition: nil), + .product(name: "GULMethodSwizzler", package: "GoogleUtilities", moduleAliases: nil, condition: nil), + .product(name: "GULNSData", package: "GoogleUtilities", moduleAliases: nil, condition: nil), + .product(name: "GULNetwork", package: "GoogleUtilities", moduleAliases: nil, condition: nil), + .product(name: "nanopb", package: "nanopb", moduleAliases: nil, condition: nil), ], publicHeadersPath: nil, type: .regular, @@ -1316,11 +1331,11 @@ extension PackageInfo { exclude: [], dependencies: [ .byName(name: "GoogleAppMeasurementWithoutAdIdSupport", condition: nil), - .product(name: "GULAppDelegateSwizzler", package: "GoogleUtilities", condition: nil), - .product(name: "GULMethodSwizzler", package: "GoogleUtilities", condition: nil), - .product(name: "GULNSData", package: "GoogleUtilities", condition: nil), - .product(name: "GULNetwork", package: "GoogleUtilities", condition: nil), - .product(name: "nanopb", package: "nanopb", condition: nil), + .product(name: "GULAppDelegateSwizzler", package: "GoogleUtilities", moduleAliases: nil, condition: nil), + .product(name: "GULMethodSwizzler", package: "GoogleUtilities", moduleAliases: nil, condition: nil), + .product(name: "GULNSData", package: "GoogleUtilities", moduleAliases: nil, condition: nil), + .product(name: "GULNetwork", package: "GoogleUtilities", moduleAliases: nil, condition: nil), + .product(name: "nanopb", package: "nanopb", moduleAliases: nil, condition: nil), ], publicHeadersPath: nil, type: .regular, diff --git a/Sources/TuistSupportTesting/TestCase/TuistTestCase.swift b/Sources/TuistSupportTesting/TestCase/TuistTestCase.swift index e71e1948ab6..80616164846 100644 --- a/Sources/TuistSupportTesting/TestCase/TuistTestCase.swift +++ b/Sources/TuistSupportTesting/TestCase/TuistTestCase.swift @@ -147,10 +147,10 @@ open class TuistTestCase: XCTestCase { let fileHandler = FileHandler() let paths = try files.map { temporaryPath.appending(try RelativePath(validating: $0)) } - try paths.forEach { - try fileHandler.touch($0) + for item in paths { + try fileHandler.touch(item) if let content { - try fileHandler.write(content, path: $0, atomically: true) + try fileHandler.write(content, path: item, atomically: true) } } return paths @@ -161,8 +161,8 @@ open class TuistTestCase: XCTestCase { let temporaryPath = try temporaryPath() let fileHandler = FileHandler.shared let paths = try folders.map { temporaryPath.appending(try RelativePath(validating: $0)) } - try paths.forEach { - try fileHandler.createFolder($0) + for path in paths { + try fileHandler.createFolder(path) } return paths } diff --git a/Sources/tuist/TuistApp.swift b/Sources/tuist/TuistApp.swift index 149d3704ed1..addec28f353 100644 --- a/Sources/tuist/TuistApp.swift +++ b/Sources/tuist/TuistApp.swift @@ -5,7 +5,7 @@ import TuistLoader import TuistSupport @main -@_documentation(visibility: internal) +@_documentation(visibility: private) private enum TuistApp { static func main() async throws { if CommandLine.arguments.contains("--verbose") { diff --git a/Sources/tuist/tuist.docc/Articles/Tuist/Tuist - Installation.md b/Sources/tuist/tuist.docc/Articles/Tuist/Tuist - Installation.md deleted file mode 100644 index ef17aa5a875..00000000000 --- a/Sources/tuist/tuist.docc/Articles/Tuist/Tuist - Installation.md +++ /dev/null @@ -1,56 +0,0 @@ -# Installation - -Learn how to install Tuist in your environment - -## Overview - -Tuist is a [command-line application](https://en.wikipedia.org/wiki/Command-line_interface) that you need to install in your environment before you can use it. The installation consists of an executable, dynamic frameworks, and a set of resources (for example, templates). Although you could manually build Tuist from the sources, we recommend using one of the following installation methods to ensure a valid installation. - -### Recommended: [rtx](https://github.com/jdx/rtx) - -Tuist defaults to [rtx](https://github.com/jdx/rtx) as a tool to deterministically manage and activate versions of Tuist. -If you don't have it installed on your system, -you can use any of these [installation methods](https://github.com/jdx/rtx#installation). -Remember to add the suggested line to your shell, which will ensure the right version is activated when you choose a Tuist project directory in your terminal session. - -> Note: rtx is recommended over alternatives like [Homebrew](https://brew.sh) because it supports scoping and activating versions to directories, ensuring every environment uses the same version of Tuist deterministically. - -Once installed, you can install Tuist through any of the following commands: - - -```bash -# Tuist -rtx install tuist@3.36.0 # Install a specific version number -rtx install tuist@3 # Install a fuzzy version number -rtx use tuist@3.36.0 # Use tuist-3.36.0 in the current project -rtx use -g tuist@3.36.0 # Use tuist-3.36.0 as the global default -rtx install tuist # Install the current version specified in .tool-versions/.rtx.toml -rtx use tuist@latest # Use the latest tuist in the current directory -rtx use -g tuist@system # Use the system's tuist as the global default - -# Tuist Cloud (For users of Tuist Cloud) -# Note: You need to install one OR the other, not both -rtx install tuist-cloud@3.36.0 -rtx install tuist-cloud@3 -rtx use tuist-cloud@3.36.0 -rtx use -g tuist-cloud@3.36.0 # Use tuist-cloud-3.36.0 as the global default -rtx install tuist-cloud # Install the current version specified in .tool-versions/.rtx.toml -rtx use tuist-cloud@latest # Use the latest tuist-cloud in the current directory -rtx use -g tuist-cloud@system # Use the system's tuist-cloud as the global default -``` - -> Tip: We recommend using `rtx use` in your Tuist projects to pin the version of Tuist across environments. The command will create a `.tool-versions` file containing the version of Tuist. - -> Important: **Tuist Cloud** (), a closed-source extension of Tuist with optimizations such as binary caching and selective testing, is distributed as a different asdf plugin, tuist-cloud. Note that by using it, you agree to the [Tuist Cloud Terms of Service](https://tuist.io/terms/). - -### Alternative: Homebrew - -If version pinning across environments is not a concern for you, -you can install Tuist using [Homebrew](https://brew.sh) and [our formulas](https://github.com/tuist/homebrew-tuist): - -```bash -brew tap tuist/tuist -brew install tuist -brew install tuist@3.36.0 -brew install tuist-cloud # If you are a Tuist Cloud user -``` diff --git a/Sources/tuist/tuist.docc/Tutorials/Tuist Tutorial/Getting-Started/Add External Dependencies/Dependencies.swift b/Sources/tuist/tuist.docc/Tutorials/Tuist Tutorial/Getting-Started/Add External Dependencies/Dependencies.swift deleted file mode 100644 index 6e7aa1b2449..00000000000 --- a/Sources/tuist/tuist.docc/Tutorials/Tuist Tutorial/Getting-Started/Add External Dependencies/Dependencies.swift +++ /dev/null @@ -1,10 +0,0 @@ -import ProjectDescription - -let dependencies = Dependencies( - swiftPackageManager: .init( - productTypes: [ - "Alamofire": .framework, // default is .staticFramework - ] - ), - platforms: [.iOS], -) diff --git a/Sources/tuist/tuist.docc/Tutorials/Tuist Tutorial/Getting-Started/Add External Dependencies/Package.swift b/Sources/tuist/tuist.docc/Tutorials/Tuist Tutorial/Getting-Started/Add External Dependencies/Package.swift deleted file mode 100644 index 4f4991d77f5..00000000000 --- a/Sources/tuist/tuist.docc/Tutorials/Tuist Tutorial/Getting-Started/Add External Dependencies/Package.swift +++ /dev/null @@ -1,9 +0,0 @@ -// swift-tools-version: 5.8 -import PackageDescription - -let package = Package( - name: "PackageName", - dependencies: [ - .package(url: "https://github.com/Alamofire/Alamofire", from: "5.0.0"), - ] -) diff --git a/Sources/tuist/tuist.docc/Tutorials/Tuist Tutorial/Getting-Started/Add External Dependencies/Tuist Tutorial - Add External Dependencies.tutorial b/Sources/tuist/tuist.docc/Tutorials/Tuist Tutorial/Getting-Started/Add External Dependencies/Tuist Tutorial - Add External Dependencies.tutorial deleted file mode 100644 index 078f250d2d1..00000000000 --- a/Sources/tuist/tuist.docc/Tutorials/Tuist Tutorial/Getting-Started/Add External Dependencies/Tuist Tutorial - Add External Dependencies.tutorial +++ /dev/null @@ -1,92 +0,0 @@ -@Tutorial(time: 5) { - @Intro(title: "Adding external Dependencies") { - Learn how to integrate external dependencies in a Tuist. - - Integrating external dependencies is an important but often painful part of Xcode project management. Dependency managers like [CocoaPods](https://cocoapods.org) integrate it when running `pod install` leveraging Xcode workspaces, and [Swift Package Manager](https://www.swift.org/package-manager) does it at build time leveraging Xcode closed build system. Both approaches might lead to integration issues that can cause compilation issues down the road. - - We are aware that's not a great developer experience, and thus we take a different approach to managing external dependencies that allow leveraging Tuist features such as linting and caching. Because we merge your project and the external dependencies' graph into a single graph, we validate and fail early if the resulting graph is invalid. - - @Image(source: "Logo-Blurred.png", alt: "Blurred Tuist Logo.") - } - - @Section(title: "Defining the dependencies") { - @ContentAndMedia { - External dependencies are defined in the `Tuist/Dependencies.swift` and `Tuist/Package.swift` manifest files. - } - - @Steps { - @Step { - Create a `Tuist/Package.swift` file defining your external dependencies and their versions with the same `Swift Package Manager` syntax you might be familiar with. - @Code(name: "Tuist/Package.swift", file: "Package.swift", reset: true) - } - - @Step { - Create a `Tuist/Dependencies.swift` file defining additional configuration details such as the desired target platform, or some custom mapping for your dependencies. - @Code(name: "Tuist/Dependencies.swift", file: "Dependencies.swift", reset: true) - - > Note: For example, here we are declaring that the `Alamofire` target should be mapped to a dynamic framework, instead of the default static framework. - } - } - } - - @Section(title: "Fetching dependencies") { - @ContentAndMedia { - After dependencies have been declared, you need to fetch them by running `tuist fetch`. Tuist uses `Swift Package Manager` to pull the dependencies under the `Tuist/Dependencies` directory, and maps them to Tuist target and projects: - - ```bash - Tuist - |- Dependencies.swift # Manifest - |- Packages.swift # Dependencies definition and versions - |- Dependencies - |- graph.json # stores the serialized dependencies graph generated by `tuist fetch` - |- Lockfiles # stores the lockfiles generated by the dependencies resolution - |- Package.resolved - |- SwiftPackageManager - |- .build # stores content of `.build/` directory generated by `Swift Package Manager` - |- artifacts - |- checkouts - |- repositories - |- manifest.db - |- workspace-state.json - |- Package.swift # the generated Package.swift - ``` - - We recommend excluding the following files and directories from [version control](https://en.wikipedia.org/wiki/Version_control) (for example, in your `.gitignore` file). - - ```bash - Tuist/Dependencies/graph.json # Avoid checking in the serialized dependencies graph generated by Tuist. - Tuist/Dependencies/Carthage # Avoid checking in build artifacts from Carthage dependencies. - Tuist/Dependencies/SwiftPackageManager # Avoid checking in build artifacts from Swift Package Manager dependencies. - ``` - } - } - - @Section(title: "Integrating dependencies into your project") { - @ContentAndMedia { - Once dependencies have been fetched, you can declare dependencies from your projects' targets. - } - - @Steps { - @Step { - Run `tuist edit` to edit your project's manifest. - @Code(name: "console", file: "tuist-edit.txt", reset: true) - } - - @Step { - Use the `.external` target dependency option to declare the dependency. - @Code(name: "Project.swift", file: "Project.swift", reset: true) - } - } - } - - - @Section(title: "Notes") { - @ContentAndMedia { - Some notes on the integration of Swift packages: - - When Swift packages are integrated as source code into your project's graph, no scheme is created for them, as you usually don't want to build only the dependency explicitly. If you need it (for example, for testing some problem in the library), you can create the scheme directly from Xcode, or you can define it in your `Project.swift` if you need it to be available across generations. - - If present, Tuist uses the product type defined in the package manifest file. Otherwise, it defaults to use `.staticFramework`. You can override the product type using the `SwiftPackageManagerDependencies.productTypes` property. - - To use Swift packages from an Objective-C target, add the path to the public headers of the package to the `HEADER_SEARCH_PATHS` of the target. The path will be something like `Tuist/Dependencies/SwiftPackageManager/.build/checkouts//`. - } - } -} - diff --git a/Sources/tuist/tuist.docc/Tutorials/Tuist Tutorial/Getting-Started/Install Uninstall Tuist/Tuist Tutorial - Install Uninstall Tuist.tutorial b/Sources/tuist/tuist.docc/Tutorials/Tuist Tutorial/Getting-Started/Install Uninstall Tuist/Tuist Tutorial - Install Uninstall Tuist.tutorial deleted file mode 100644 index 6c856c8272f..00000000000 --- a/Sources/tuist/tuist.docc/Tutorials/Tuist Tutorial/Getting-Started/Install Uninstall Tuist/Tuist Tutorial - Install Uninstall Tuist.tutorial +++ /dev/null @@ -1,45 +0,0 @@ -@Tutorial(time: 2) { - @Intro(title: "Install/Uninstall Tuist") { - Learn how to install tuist and manage it. - - @Image(source: "Logo-Blurred.png", alt: "Blurred Tuist Logo.") - } - - @Section(title: "Install Tuist") { - @ContentAndMedia { - The first thing that we need to do to get started is install tuist. - } - - @Steps { - @Step { - Install the tool by runnning this script in your terminal. - @Code(name: "console", file: "tuist-install-curl.txt", reset: true) - - > Note: The process is relatively fast because we are actually not installing the tool. We are installing tuistenv (which gets renamed to tuist) when you install it. - } - - @Step { - You can check if tuist has been installed correctly by getting its version. - @Code(name: "console", file: "tuist-version.txt") - } - - @Step { - You can also see the list of available commands, if you are curious. - @Code(name: "console", file: "tuist-help.txt") - } - } - } - - @Section(title: "Uninstall Tuist") { - @ContentAndMedia { - Uninstalling tuist is as easy as installing it. - } - - @Steps { - @Step { - Uninstall the tool by runnning this script in your terminal. - @Code(name: "console", file: "tuist-uninstall-curl.txt", reset: true) - } - } - } -} diff --git a/Sources/tuist/tuist.docc/Tutorials/Tuist Tutorial/Getting-Started/Install Uninstall Tuist/tuist-install-curl.txt b/Sources/tuist/tuist.docc/Tutorials/Tuist Tutorial/Getting-Started/Install Uninstall Tuist/tuist-install-curl.txt deleted file mode 100644 index 6306095f4a9..00000000000 --- a/Sources/tuist/tuist.docc/Tutorials/Tuist Tutorial/Getting-Started/Install Uninstall Tuist/tuist-install-curl.txt +++ /dev/null @@ -1 +0,0 @@ -% curl -Ls https://install.tuist.io | bash diff --git a/Sources/tuist/tuist.docc/Tutorials/Tuist Tutorial/Getting-Started/Install Uninstall Tuist/tuist-uninstall-curl.txt b/Sources/tuist/tuist.docc/Tutorials/Tuist Tutorial/Getting-Started/Install Uninstall Tuist/tuist-uninstall-curl.txt deleted file mode 100644 index 9a8713f28b7..00000000000 --- a/Sources/tuist/tuist.docc/Tutorials/Tuist Tutorial/Getting-Started/Install Uninstall Tuist/tuist-uninstall-curl.txt +++ /dev/null @@ -1 +0,0 @@ -% curl -Ls https://raw.githubusercontent.com/tuist/tuist/main/script/uninstall | bash diff --git a/Sources/tuist/tuist.docc/Tutorials/Tuist Tutorial/Getting-Started/Install Uninstall Tuist/tuist-version.txt b/Sources/tuist/tuist.docc/Tutorials/Tuist Tutorial/Getting-Started/Install Uninstall Tuist/tuist-version.txt deleted file mode 100644 index ece5ac7bd6b..00000000000 --- a/Sources/tuist/tuist.docc/Tutorials/Tuist Tutorial/Getting-Started/Install Uninstall Tuist/tuist-version.txt +++ /dev/null @@ -1,6 +0,0 @@ -% curl -Ls https://install.tuist.io | bash - -% tuist version -3.25.0 - -# You should get a semantic version like shown above. diff --git a/Sources/tuistenv/main.swift b/Sources/tuistenv/main.swift index 9a3a59cd9ca..9e7d74a134f 100644 --- a/Sources/tuistenv/main.swift +++ b/Sources/tuistenv/main.swift @@ -5,4 +5,15 @@ import TuistSupport try TuistSupport.Environment.shared.bootstrap() LogOutput.bootstrap() +WarningController.shared.append(warning: """ +The method used to install this version of Tuist is deprecated and will be deleted soon. +Please, uninstall it by running: + + curl -Ls https://uninstall.tuist.io | bash + +And follow the new installation instructions at https://docs.tuist.io/documentation/tuist/installation + +Read more about the rationale at https://tuist.io/blog/2023/12/15/rtx-default/ +""") + TuistCommand.main() diff --git a/Sources/tuistfixturegenerator/Generator/Generator.swift b/Sources/tuistfixturegenerator/Generator/Generator.swift index b57243aeaae..cd868cad6a3 100644 --- a/Sources/tuistfixturegenerator/Generator/Generator.swift +++ b/Sources/tuistfixturegenerator/Generator/Generator.swift @@ -26,10 +26,10 @@ class Generator { projects: projects ) - try projects.forEach { + for project in projects { try initProject( at: rootPath, - name: $0 + name: project ) } } @@ -61,8 +61,8 @@ class Generator { try fileSystem.createDirectory(projectPath) try initProjectManifest(at: projectPath, name: name, targets: targets) - try targets.forEach { - try initTarget(at: projectPath, name: $0) + for target in targets { + try initTarget(at: projectPath, name: target) } } @@ -94,9 +94,9 @@ class Generator { let sourcesPath = path.appending(component: "Sources") try fileSystem.createDirectory(sourcesPath) - try (1 ... config.sources).forEach { - let sourceName = "Source\($0).swift" - let source = sourceTemplate.generate(frameworkName: targetName, number: $0) + for item in 1 ... config.sources { + let sourceName = "Source\(item).swift" + let source = sourceTemplate.generate(frameworkName: targetName, number: item) try fileSystem.writeFileContents( sourcesPath.appending(component: sourceName), bytes: ByteString(encodingAsUTF8: source) diff --git a/Tests/ProjectDescriptionTests/EnvironmentTests.swift b/Tests/ProjectDescriptionTests/EnvironmentTests.swift index 76ad0cca5d1..68b5cc6bd3b 100644 --- a/Tests/ProjectDescriptionTests/EnvironmentTests.swift +++ b/Tests/ProjectDescriptionTests/EnvironmentTests.swift @@ -12,7 +12,7 @@ final class EnvironmentTests: XCTestCase { "TUIST_3": "yes", "TUIST_4": "YES", ] - environment.enumerated().forEach { index, _ in + for (index, _) in environment.enumerated() { let value = Environment.value(for: String(index), environment: environment) XCTAssertTrue(value.getBoolean(default: false)) } @@ -26,7 +26,7 @@ final class EnvironmentTests: XCTestCase { "TUIST_3": "no", "TUIST_4": "NO", ] - environment.enumerated().forEach { index, _ in + for (index, _) in environment.enumerated() { let value = Environment.value(for: String(index), environment: environment) XCTAssertFalse(value.getBoolean(default: true)) } @@ -38,7 +38,7 @@ final class EnvironmentTests: XCTestCase { "TUIST_0": stringValue, "TUIST_1": "1", ] - environment.enumerated().forEach { index, _ in + for (index, _) in environment.enumerated() { let value = Environment.value(for: String(index), environment: environment) XCTAssertEqual(value.getString(default: ""), environment["TUIST_\(index)"]) } diff --git a/Tests/TuistAcceptanceTests/BuildRulesAcceptanceTests.swift b/Tests/TuistAcceptanceTests/BuildRulesAcceptanceTests.swift new file mode 100644 index 00000000000..64b09951dd8 --- /dev/null +++ b/Tests/TuistAcceptanceTests/BuildRulesAcceptanceTests.swift @@ -0,0 +1,18 @@ +import TSCBasic +import TuistAcceptanceTesting +import TuistSupport +import TuistSupportTesting +import XcodeProj +import XCTest + +final class BuildRulesAcceptanceTestAppWithBuildRules: TuistAcceptanceTestCase { + func test_app_with_build_rules() async throws { + try setUpFixture(.appWithBuildRules) + try await run(GenerateCommand.self) + try await run(BuildCommand.self) + let xcodeproj = try XcodeProj(pathString: xcodeprojPath.pathString) + let target = try XCTUnwrapTarget("App", in: xcodeproj) + let buildRule = try XCTUnwrap(target.buildRules.first(where: { $0.name == "Process_InfoPlist.strings" })) + XCTAssertEqual(buildRule.filePatterns, "*/InfoPlist.strings") + } +} diff --git a/Tests/TuistAcceptanceTests/EditAcceptanceTests.swift b/Tests/TuistAcceptanceTests/EditAcceptanceTests.swift new file mode 100644 index 00000000000..72feb61a1d4 --- /dev/null +++ b/Tests/TuistAcceptanceTests/EditAcceptanceTests.swift @@ -0,0 +1,58 @@ +import TSCBasic +import TuistAcceptanceTesting +import TuistSupport +import TuistSupportTesting +import XcodeProj +import XCTest + +final class EditAcceptanceTestiOSAppWithHelpers: TuistAcceptanceTestCase { + func test_ios_app_with_helpers() async throws { + try setUpFixture(.iosAppWithHelpers) + try await run(EditCommand.self) + try build(scheme: "Manifests") + } +} + +final class EditAcceptanceTestPlugin: TuistAcceptanceTestCase { + func test_plugin() async throws { + try setUpFixture(.plugin) + try await run(EditCommand.self) + try build(scheme: "Plugins") + } +} + +final class EditAcceptanceTestAppWithPlugins: TuistAcceptanceTestCase { + func test_app_with_plugins() async throws { + try setUpFixture(.appWithPlugins) + try await run(FetchCommand.self) + try await run(EditCommand.self) + try build(scheme: "Manifests") + try build(scheme: "Plugins") + try build(scheme: "LocalPlugin") + } +} + +final class EditAcceptanceTestAppWithSPMDependencies: TuistAcceptanceTestCase { + func test_app_with_spm_dependencies() async throws { + try setUpFixture(.appWithSpmDependencies) + try await run(EditCommand.self) + try build(scheme: "Manifests") + } +} + +extension TuistAcceptanceTestCase { + fileprivate func build(scheme: String) throws { + try System.shared.runAndPrint( + [ + "/usr/bin/xcrun", + "xcodebuild", + "clean", + "build", + "-scheme", + scheme, + "-workspace", + workspacePath.pathString, + ] + ) + } +} diff --git a/Tests/TuistAcceptanceTests/GraphAcceptanceTests.swift b/Tests/TuistAcceptanceTests/GraphAcceptanceTests.swift new file mode 100644 index 00000000000..1c35777faf2 --- /dev/null +++ b/Tests/TuistAcceptanceTests/GraphAcceptanceTests.swift @@ -0,0 +1,30 @@ +import TSCBasic +import TuistAcceptanceTesting +import TuistSupport +import TuistSupportTesting +import XcodeProj +import XCTest + +// TODO: Fix (issues with finding executables) +// final class GraphAcceptanceTestiOSWorkspaceWithMicrofeatureArchitecture: TuistAcceptanceTestCase { +// func test_ios_workspace_with_microfeature_architecture() async throws { +// try setUpFixture("ios_workspace_with_microfeature_architecture") +// try await run(GraphCommand.self, "--output-path", fixturePath.pathString) +// let graphFile = fixturePath.appending(component: "graph.png") +// try System.shared.runAndPrint( +// [ +// "file", +// graphFile.pathString, +// ] +// ) +// try FileHandler.shared.delete(graphFile) +// +// try await run(GraphCommand.self, "--output-path", fixturePath.pathString, "Data") +// try System.shared.runAndPrint( +// [ +// "file", +// graphFile.pathString, +// ] +// ) +// } +// } diff --git a/Tests/TuistAcceptanceTests/InitAcceptanceTests.swift b/Tests/TuistAcceptanceTests/InitAcceptanceTests.swift new file mode 100644 index 00000000000..8379f30d69b --- /dev/null +++ b/Tests/TuistAcceptanceTests/InitAcceptanceTests.swift @@ -0,0 +1,57 @@ +import TSCBasic +import TuistAcceptanceTesting +import TuistSupport +import TuistSupportTesting +import XcodeProj +import XCTest + +final class InitAcceptanceTestmacOSApp: TuistAcceptanceTestCase { + func test_init_macos_app() async throws { + try run(InitCommand.self, "--platform", "macos", "--name", "Test") + try await run(BuildCommand.self) + } +} + +final class InitAcceptanceTestiOSApp: TuistAcceptanceTestCase { + func test_init_ios_app() async throws { + try run(InitCommand.self, "--platform", "ios", "--name", "My-App") + try await run(BuildCommand.self) + } +} + +// TODO: Fix +// final class InitAcceptanceTesttvOSApp: TuistAcceptanceTestCase { +// func test_init_tvos_app() async throws { +// try run(InitCommand.self, "--platform", "tvos", "--name", "TvApp") +// try await run(BuildCommand.self) +// } +// } + +final class InitAcceptanceTestSwiftUIiOSApp: TuistAcceptanceTestCase { + func test_init_swift_ui_ios_app() async throws { + try run(InitCommand.self, "--platform", "ios", "--name", "MyApp", "--template", "swiftui") + try await run(BuildCommand.self) + } +} + +final class InitAcceptanceTestSwiftUImacOSApp: TuistAcceptanceTestCase { + func test_init_swift_ui_macos_app() async throws { + try run(InitCommand.self, "--platform", "macos", "--name", "MyApp", "--template", "swiftui") + try await run(BuildCommand.self) + } +} + +// TODO: Fix +// final class InitAcceptanceTestSwiftUtvOSApp: TuistAcceptanceTestCase { +// func test_init_swift_ui_tvos_app() async throws { +// try run(InitCommand.self, "--platform", "tvos", "--name", "MyApp", "--template", "swiftui") +// try await run(BuildCommand.self) +// } +// } + +final class InitAcceptanceTestCLIProjectWithTemplateInADifferentRepository: TuistAcceptanceTestCase { + func test_cli_project_with_template_in_a_different_repository() async throws { + try run(InitCommand.self, "--template", "https://github.com/tuist/ExampleTuistTemplate.git", "--name", "MyApp") + try await run(BuildCommand.self) + } +} diff --git a/Tests/TuistAcceptanceTests/ListTargetsAcceptanceTests.swift b/Tests/TuistAcceptanceTests/ListTargetsAcceptanceTests.swift new file mode 100644 index 00000000000..370d3d426ac --- /dev/null +++ b/Tests/TuistAcceptanceTests/ListTargetsAcceptanceTests.swift @@ -0,0 +1,38 @@ +import TSCBasic +import TuistAcceptanceTesting +import TuistSupport +import TuistSupportTesting +import XcodeProj +import XCTest + +final class ListTargetsAcceptanceTestiOSWorkspaceWithMicrofeatureArchitecture: TuistAcceptanceTestCase { + func test_ios_workspace_with_microfeature_architecture() async throws { + try setUpFixture(.iosWorkspaceWithMicrofeatureArchitecture) + try await run(GenerateCommand.self) + try listTargets(for: "UIComponents") + try listTargets(for: "Core") + try listTargets(for: "Data") + } +} + +extension TuistAcceptanceTestCase { + fileprivate func listTargets( + for framework: String + ) throws { + let frameworkXcodeprojPath = fixturePath.appending( + components: [ + "Frameworks", + "\(framework)Framework", + "\(framework).xcodeproj", + ] + ) + + try run(MigrationTargetsByDependenciesCommand.self, "-p", frameworkXcodeprojPath.pathString) + XCTAssertStandardOutput( + pattern: + """ + "targetName" : "\(framework)" + """ + ) + } +} diff --git a/Tests/TuistAcceptanceTests/PluginAcceptanceTests.swift b/Tests/TuistAcceptanceTests/PluginAcceptanceTests.swift new file mode 100644 index 00000000000..bf712ce142c --- /dev/null +++ b/Tests/TuistAcceptanceTests/PluginAcceptanceTests.swift @@ -0,0 +1,23 @@ +import TSCBasic +import TuistAcceptanceTesting +import TuistSupport +import TuistSupportTesting +import XcodeProj +import XCTest + +final class PluginAcceptanceTestTuistPlugin: TuistAcceptanceTestCase { + func test_tuist_plugin() async throws { + try setUpFixture(.tuistPlugin) + try run(PluginBuildCommand.self) + try run(PluginRunCommand.self, "tuist-create-file") + } +} + +final class PluginAcceptanceTestAppWithPlugins: TuistAcceptanceTestCase { + func test_app_with_plugins() async throws { + try setUpFixture(.appWithPlugins) + try await run(FetchCommand.self) + try await run(GenerateCommand.self) + try await run(BuildCommand.self) + } +} diff --git a/Tests/TuistAcceptanceTests/PrecompiledAcceptanceTests.swift b/Tests/TuistAcceptanceTests/PrecompiledAcceptanceTests.swift new file mode 100644 index 00000000000..d205ec5e50a --- /dev/null +++ b/Tests/TuistAcceptanceTests/PrecompiledAcceptanceTests.swift @@ -0,0 +1,102 @@ +import TSCBasic +import TuistAcceptanceTesting +import TuistSupport +import TuistSupportTesting +import XcodeProj +import XCTest + +final class PrecomiledAcceptanceTestiOSAppWithStaticFrameworks: TuistAcceptanceTestCase { + func test_ios_app_with_static_frameworks() async throws { + try setUpFixture(.iosAppWithStaticFrameworks) + try await run(GenerateCommand.self) + try await run(BuildCommand.self) + } +} + +final class PrecomiledAcceptanceTestiOSAppWithStaticLibraries: TuistAcceptanceTestCase { + func test_ios_app_with_static_libraries() async throws { + try setUpFixture(.iosAppWithStaticLibraries) + try await run(GenerateCommand.self) + try await run(BuildCommand.self) + } +} + +final class PrecomiledAcceptanceTestiOSAppWithTransitiveFramework: TuistAcceptanceTestCase { + func test_ios_app_with_transitive_framework() async throws { + try setUpFixture(.iosAppWithTransitiveFramework) + try await run(GenerateCommand.self) + try await run(BuildCommand.self, "App", "--platform", "iOS") + try await XCTAssertProductWithDestinationContainsFrameworkWithArchitecture( + framework: "Framework1", + architecture: "x86_64" + ) + try XCTAssertProductWithDestinationDoesNotContainHeaders( + "App.app", + destination: "Debug-iphonesimulator" + ) + try await run(BuildCommand.self, "Framework1-iOS", "--platform", "iOS") + try await run(BuildCommand.self, "Framework1-macOS", "--platform", "macOS") + try await run(BuildCommand.self, "Framework1Tests-iOS", "--platform", "iOS") + try await run(BuildCommand.self, "Framework1Tests-macOS", "--platform", "macOS") + try await run(BuildCommand.self, "StaticFramework1", "--platform", "iOS") + } +} + +final class PrecompiledAcceptanceTestiOSAppWithStaticLibraryAndPackage: TuistAcceptanceTestCase { + func test_ios_app_with_static_library_and_package() async throws { + try setUpFixture(.iosAppWithStaticLibraryAndPackage) + try await run(GenerateCommand.self) + try await run(BuildCommand.self) + } +} + +final class PrecompiledAcceptanceTestiOSAppWithXCFrameworks: TuistAcceptanceTestCase { + func test_ios_app_with_xcframeworks() async throws { + try setUpFixture(.iosAppWithXcframeworks) + try await run(GenerateCommand.self) + try await run(BuildCommand.self) + try await XCTAssertProductWithDestinationContainsFrameworkWithArchitecture( + framework: "MyFramework", + architecture: "x86_64" + ) + try XCTAssertProductWithDestinationDoesNotContainHeaders( + "App.app", + destination: "Debug-iphonesimulator" + ) + } +} + +extension TuistAcceptanceTestCase { + func XCTAssertProductWithDestinationContainsFrameworkWithArchitecture( + _ product: String = "App.app", + destination: String = "Debug-iphonesimulator", + framework: String, + architecture: String, + file: StaticString = #file, + line: UInt = #line + ) async throws { + let productPath = try productPath( + for: product, + destination: destination + ) + + guard let frameworkPath = FileHandler.shared.glob(productPath, glob: "**/Frameworks/\(framework).framework").first, + FileHandler.shared.exists(frameworkPath) + else { + XCTFail( + "Framework \(framework) not found for product \(product) and destination \(destination)", + file: file, + line: line + ) + return + } + + let fileInfo = try await System.shared.runAndCollectOutput( + [ + "file", + frameworkPath.appending(component: framework).pathString, + ] + ) + XCTAssertTrue(fileInfo.standardOutput.contains(architecture)) + } +} diff --git a/Tests/TuistAcceptanceTests/RunAcceptanceTests.swift b/Tests/TuistAcceptanceTests/RunAcceptanceTests.swift new file mode 100644 index 00000000000..9348443ba3a --- /dev/null +++ b/Tests/TuistAcceptanceTests/RunAcceptanceTests.swift @@ -0,0 +1,13 @@ +import TSCBasic +import TuistAcceptanceTesting +import TuistSupport +import TuistSupportTesting +import XcodeProj +import XCTest + +final class RunAcceptanceTestCommandLineToolBasic: TuistAcceptanceTestCase { + func test_command_line_tool_basic() async throws { + try setUpFixture(.commandLineToolBasic) + try await run(RunCommand.self, "CommandLineTool") + } +} diff --git a/Tests/TuistAcceptanceTests/ScaffoldAcceptanceTests.swift b/Tests/TuistAcceptanceTests/ScaffoldAcceptanceTests.swift new file mode 100644 index 00000000000..d39e7eaa154 --- /dev/null +++ b/Tests/TuistAcceptanceTests/ScaffoldAcceptanceTests.swift @@ -0,0 +1,144 @@ +import TSCBasic +import TuistAcceptanceTesting +import TuistSupport +import TuistSupportTesting +import XcodeProj +import XCTest + +final class ScaffoldAcceptanceTests: TuistAcceptanceTestCase { + override func tearDown() { + ScaffoldCommand.requiredTemplateOptions = [] + ScaffoldCommand.optionalTemplateOptions = [] + super.tearDown() + } + + func test_ios_app_with_templates_custom() async throws { + try setUpFixture(.iosAppWithTemplates) + try await run(FetchCommand.self) + try await ScaffoldCommand.preprocess([ + "scaffold", + "custom", + "--name", + "TemplateProject", + "--path", + fixturePath.pathString, + ]) + try await run(ScaffoldCommand.self, "custom", "--name", "TemplateProject") + let templateProjectDirectory = fixturePath.appending(component: "TemplateProject") + XCTAssertEqual( + try FileHandler.shared.readTextFile(templateProjectDirectory.appending(component: "custom.swift")), + "// this is test TemplateProject content" + ) + XCTAssertEqual( + try FileHandler.shared.readTextFile(templateProjectDirectory.appending(component: "generated.swift")), + """ + // Generated file with platform: ios and name: TemplateProject + + """ + ) + } + + func test_ios_app_with_templates_custom_using_filters() async throws { + try setUpFixture(.iosAppWithTemplates) + try await run(FetchCommand.self) + try await ScaffoldCommand.preprocess([ + "scaffold", + "custom_using_filters", + "--name", + "TemplateProject", + "--path", + fixturePath.pathString, + ]) + try await run(ScaffoldCommand.self, "custom_using_filters", "--name", "TemplateProject") + let templateProjectDirectory = fixturePath.appending(component: "TemplateProject") + XCTAssertEqual( + try FileHandler.shared.readTextFile(templateProjectDirectory.appending(component: "custom.swift")), + "// this is test TemplateProject content" + ) + } + + func test_ios_app_with_templates_custom_using_copy_folder() async throws { + try setUpFixture(.iosAppWithTemplates) + try await run(FetchCommand.self) + try await ScaffoldCommand.preprocess([ + "scaffold", + "custom_using_copy_folder", + "--name", + "TemplateProject", + "--path", + fixturePath.pathString, + ]) + try await run(ScaffoldCommand.self, "custom_using_copy_folder", "--name", "TemplateProject") + let templateProjectDirectory = fixturePath.appending(component: "TemplateProject") + XCTAssertEqual( + try FileHandler.shared.readTextFile(templateProjectDirectory.appending(component: "generated.swift")), + """ + // Generated file with platform: ios and name: TemplateProject + + """ + ) + XCTAssertEqual( + try FileHandler.shared.readTextFile( + templateProjectDirectory.appending(components: ["sourceFolder", "file1.txt"]) + ), + """ + Content of file 1 + + """ + ) + XCTAssertEqual( + try FileHandler.shared.readTextFile( + templateProjectDirectory.appending(components: ["sourceFolder", "subFolder", "file2.txt"]) + ), + """ + Content of file 2 + + """ + ) + } + + func test_app_with_plugins_local_plugin() async throws { + try setUpFixture(.appWithPlugins) + try await run(FetchCommand.self) + try await ScaffoldCommand.preprocess(["scaffold", "custom", "--name", "PluginTemplate", "--path", fixturePath.pathString]) + try await run(ScaffoldCommand.self, "custom", "--name", "PluginTemplate") + let pluginTemplateDirectory = fixturePath.appending(component: "PluginTemplate") + XCTAssertEqual( + try FileHandler.shared.readTextFile(pluginTemplateDirectory.appending(component: "custom.swift")), + "// this is test PluginTemplate content" + ) + XCTAssertEqual( + try FileHandler.shared.readTextFile(pluginTemplateDirectory.appending(component: "generated.swift")), + """ + // Generated file with platform: ios and name: PluginTemplate + + """ + ) + } + + func test_app_with_plugins_remote_plugin() async throws { + try setUpFixture(.appWithPlugins) + try await run(FetchCommand.self) + try await ScaffoldCommand.preprocess([ + "scaffold", + "custom_two", + "--name", + "PluginTemplate", + "--path", + fixturePath.pathString, + ]) + try await run(ScaffoldCommand.self, "custom_two", "--name", "PluginTemplate") + let pluginTemplateDirectory = fixturePath.appending(component: "PluginTemplate") + XCTAssertEqual( + try FileHandler.shared.readTextFile(pluginTemplateDirectory.appending(component: "custom.swift")), + "// this is test PluginTemplate content" + ) + XCTAssertEqual( + try FileHandler.shared.readTextFile(pluginTemplateDirectory.appending(component: "generated.swift")), + """ + // Generated file with platform: ios and name: PluginTemplate + + """ + ) + } +} diff --git a/Tests/TuistAutomationIntegrationTests/XcodeBuild/XcodeBuildControllerIntegrationTests.swift b/Tests/TuistAutomationIntegrationTests/XcodeBuild/XcodeBuildControllerIntegrationTests.swift index 19bd37ab2d7..8e35143e554 100644 --- a/Tests/TuistAutomationIntegrationTests/XcodeBuild/XcodeBuildControllerIntegrationTests.swift +++ b/Tests/TuistAutomationIntegrationTests/XcodeBuild/XcodeBuildControllerIntegrationTests.swift @@ -25,7 +25,7 @@ final class XcodeBuildControllerIntegrationTests: TuistTestCase { let target = XcodeBuildTarget.project(fixturePath(path: try RelativePath(validating: "Frameworks/Frameworks.xcodeproj"))) // When - let got = try await subject.showBuildSettings(target, scheme: "iOS", configuration: "Debug") + let got = try await subject.showBuildSettings(target, scheme: "iOS", configuration: "Debug", derivedDataPath: nil) // Then XCTAssertEqual(got.count, 1) diff --git a/Tests/TuistAutomationTests/Utilities/TargetBuilderTests.swift b/Tests/TuistAutomationTests/Utilities/TargetBuilderTests.swift index 9a65e6eb96f..8ed3206b38d 100644 --- a/Tests/TuistAutomationTests/Utilities/TargetBuilderTests.swift +++ b/Tests/TuistAutomationTests/Utilities/TargetBuilderTests.swift @@ -82,15 +82,16 @@ final class TargetBuilderTests: TuistUnitTestCase { buildArguments } - xcodeBuildController.buildStub = { _workspace, _scheme, _destination, _rosetta, _, _clean, _buildArguments, _ in - XCTAssertEqual(_workspace.path, workspacePath) - XCTAssertEqual(_scheme, scheme.name) - XCTAssertEqual(_destination, destination) - XCTAssertEqual(_rosetta, rosetta) - XCTAssertEqual(_clean, clean) - XCTAssertEqual(_buildArguments, buildArguments) - return [.standardOutput(.init(raw: "success"))] - } + xcodeBuildController + .buildStub = { _workspace, _scheme, _destination, _rosetta, _, _clean, _buildArguments in + XCTAssertEqual(_workspace.path, workspacePath) + XCTAssertEqual(_scheme, scheme.name) + XCTAssertEqual(_destination, destination) + XCTAssertEqual(_rosetta, rosetta) + XCTAssertEqual(_clean, clean) + XCTAssertEqual(_buildArguments, buildArguments) + return [.standardOutput(.init(raw: "success"))] + } // When try await subject.buildTarget( @@ -105,8 +106,7 @@ final class TargetBuilderTests: TuistUnitTestCase { device: device, osVersion: version, rosetta: rosetta, - graphTraverser: MockGraphTraverser(), - rawXcodebuildLogs: false + graphTraverser: MockGraphTraverser() ) } @@ -118,7 +118,7 @@ final class TargetBuilderTests: TuistUnitTestCase { let workspacePath = try AbsolutePath(validating: "/path/to/project.xcworkspace") let graphTraverser = MockGraphTraverser() - xcodeBuildController.buildStub = { _, _, _, _, _, _, _, _ in + xcodeBuildController.buildStub = { _, _, _, _, _, _, _ in [.standardOutput(.init(raw: "success"))] } @@ -142,8 +142,7 @@ final class TargetBuilderTests: TuistUnitTestCase { device: nil, osVersion: nil, rosetta: false, - graphTraverser: graphTraverser, - rawXcodebuildLogs: false + graphTraverser: graphTraverser ) // Then @@ -170,7 +169,7 @@ final class TargetBuilderTests: TuistUnitTestCase { let workspacePath = try AbsolutePath(validating: "/path/to/project.xcworkspace") let graphTraverser = MockGraphTraverser() - xcodeBuildController.buildStub = { _, _, _, _, _, _, _, _ in + xcodeBuildController.buildStub = { _, _, _, _, _, _, _ in [.standardOutput(.init(raw: "success"))] } @@ -194,8 +193,7 @@ final class TargetBuilderTests: TuistUnitTestCase { device: nil, osVersion: nil, rosetta: false, - graphTraverser: graphTraverser, - rawXcodebuildLogs: false + graphTraverser: graphTraverser ) // Then diff --git a/Tests/TuistAutomationTests/Utilities/TargetRunnerTests.swift b/Tests/TuistAutomationTests/Utilities/TargetRunnerTests.swift index e39b08bdcff..6355e435b4e 100644 --- a/Tests/TuistAutomationTests/Utilities/TargetRunnerTests.swift +++ b/Tests/TuistAutomationTests/Utilities/TargetRunnerTests.swift @@ -158,7 +158,7 @@ final class TargetRunnerTests: TuistUnitTestCase { fileHandler.stubExists = { _ in true } xcodeProjectBuildDirectoryLocator.locateStub = { _, _, _, _ in outputPath } - xcodeBuildController.showBuildSettingsStub = { _, _, _ in + xcodeBuildController.showBuildSettingsStub = { _, _, _, _ in let settings = ["PRODUCT_BUNDLE_IDENTIFIER": bundleId] return [ graphTarget.target diff --git a/Tests/TuistAutomationTests/XcodeBuild/XcodeBuildControllerTests.swift b/Tests/TuistAutomationTests/XcodeBuild/XcodeBuildControllerTests.swift index f131c79371f..60bddb92740 100644 --- a/Tests/TuistAutomationTests/XcodeBuild/XcodeBuildControllerTests.swift +++ b/Tests/TuistAutomationTests/XcodeBuild/XcodeBuildControllerTests.swift @@ -50,8 +50,7 @@ final class XcodeBuildControllerTests: TuistUnitTestCase { rosetta: false, derivedDataPath: nil, clean: true, - arguments: [], - rawXcodebuildLogs: false + arguments: [] ) let result = try await events.toArray() @@ -79,8 +78,7 @@ final class XcodeBuildControllerTests: TuistUnitTestCase { rosetta: true, derivedDataPath: nil, clean: true, - arguments: [], - rawXcodebuildLogs: false + arguments: [] ) let result = try await events.toArray() @@ -109,8 +107,7 @@ final class XcodeBuildControllerTests: TuistUnitTestCase { rosetta: false, derivedDataPath: nil, clean: true, - arguments: [], - rawXcodebuildLogs: false + arguments: [] ) let result = try await events.toArray() @@ -139,8 +136,7 @@ final class XcodeBuildControllerTests: TuistUnitTestCase { rosetta: true, derivedDataPath: nil, clean: true, - arguments: [], - rawXcodebuildLogs: false + arguments: [] ) let result = try await events.toArray() @@ -181,8 +177,7 @@ final class XcodeBuildControllerTests: TuistUnitTestCase { retryCount: 0, testTargets: [], skipTestTargets: [], - testPlanConfiguration: nil, - rawXcodebuildLogs: false + testPlanConfiguration: nil ) let result = try await events.toArray() @@ -223,8 +218,7 @@ final class XcodeBuildControllerTests: TuistUnitTestCase { retryCount: 0, testTargets: [], skipTestTargets: [], - testPlanConfiguration: nil, - rawXcodebuildLogs: false + testPlanConfiguration: nil ) let result = try await events.toArray() @@ -267,8 +261,7 @@ final class XcodeBuildControllerTests: TuistUnitTestCase { retryCount: 0, testTargets: [], skipTestTargets: [], - testPlanConfiguration: nil, - rawXcodebuildLogs: false + testPlanConfiguration: nil ) let result = try await events.toArray() @@ -311,8 +304,7 @@ final class XcodeBuildControllerTests: TuistUnitTestCase { retryCount: 0, testTargets: [], skipTestTargets: [], - testPlanConfiguration: nil, - rawXcodebuildLogs: false + testPlanConfiguration: nil ) let result = try await events.toArray() @@ -355,8 +347,7 @@ final class XcodeBuildControllerTests: TuistUnitTestCase { retryCount: 0, testTargets: [], skipTestTargets: [], - testPlanConfiguration: nil, - rawXcodebuildLogs: false + testPlanConfiguration: nil ) let result = try await events.toArray() diff --git a/Tests/TuistBuildAcceptanceTests/BuildAcceptanceTests.swift b/Tests/TuistBuildAcceptanceTests/BuildAcceptanceTests.swift index d92dbd98452..2bd23483c37 100644 --- a/Tests/TuistBuildAcceptanceTests/BuildAcceptanceTests.swift +++ b/Tests/TuistBuildAcceptanceTests/BuildAcceptanceTests.swift @@ -16,7 +16,7 @@ final class BuildAcceptanceTestWithTemplates: TuistAcceptanceTestCase { final class BuildAcceptanceTestAppWithFrameworkAndTests: TuistAcceptanceTestCase { func test_with_framework_and_tests() async throws { - try setUpFixture("app_with_framework_and_tests") + try setUpFixture(.appWithFrameworkAndTests) try await run(GenerateCommand.self) try await run(BuildCommand.self) try await run(BuildCommand.self, "App") @@ -40,7 +40,7 @@ final class BuildAcceptanceTestAppWithFrameworkAndTests: TuistAcceptanceTestCase final class BuildAcceptanceTestiOSAppWithCustomConfigurationAndBuildToCustomDirectory: TuistAcceptanceTestCase { func test_ios_app_with_custom_and_build_to_custom_directory() async throws { - try setUpFixture("ios_app_with_custom_configuration") + try setUpFixture(.iosAppWithCustomConfiguration) try await run(GenerateCommand.self) try await run( BuildCommand.self, @@ -81,7 +81,7 @@ final class BuildAcceptanceTestiOSAppWithCustomConfigurationAndBuildToCustomDire final class BuildAcceptanceTestFrameworkWithSwiftMacroIntegratedWithStandardMethod: TuistAcceptanceTestCase { func test_framework_with_swift_macro_integrated_with_standard_method() async throws { - try setUpFixture("framework_with_swift_macro") + try setUpFixture(.frameworkWithSwiftMacro) try await run(GenerateCommand.self) try await run(BuildCommand.self, "Framework") } @@ -89,18 +89,34 @@ final class BuildAcceptanceTestFrameworkWithSwiftMacroIntegratedWithStandardMeth final class BuildAcceptanceTestFrameworkWithSwiftMacroIntegratedWithXcodeProjPrimitives: TuistAcceptanceTestCase { func test_framework_with_swift_macro_integrated_with_xcode_proj_primitives() async throws { - try setUpFixture("framework_with_native_swift_macro") + try setUpFixture(.frameworkWithNativeSwiftMacro) try await run(FetchCommand.self) try await run(BuildCommand.self, "Framework", "--platform", "macos") try await run(BuildCommand.self, "Framework", "--platform", "ios") } } +final class BuildAcceptanceTestMultiplatformAppWithExtensions: TuistAcceptanceTestCase { + func test() async throws { + try setUpFixture(.multiplatformAppWithExtension) + try await run(GenerateCommand.self) + try await run(BuildCommand.self, "App", "--platform", "ios") + } +} + final class BuildAcceptanceTestMultiplatformAppWithSDK: TuistAcceptanceTestCase { func test() async throws { - try setUpFixture("multiplatform_app_with_sdk") + try setUpFixture(.multiplatformAppWithSdk) try await run(FetchCommand.self) try await run(BuildCommand.self, "App", "--platform", "macos") try await run(BuildCommand.self, "App", "--platform", "ios") } } + +final class BuildAcceptanceTestAppWithSPMDependencies: TuistAcceptanceTestCase { + func test() async throws { + try setUpFixture(.appWithSpmDependencies) + try await run(FetchCommand.self) + try await run(BuildCommand.self, "App", "--platform", "ios") + } +} diff --git a/Tests/TuistCoreTests/Automation/XcodeBuildControllingTests.swift b/Tests/TuistCoreTests/Automation/XcodeBuildControllingTests.swift new file mode 100644 index 00000000000..5cf87cd3417 --- /dev/null +++ b/Tests/TuistCoreTests/Automation/XcodeBuildControllingTests.swift @@ -0,0 +1,32 @@ +import Foundation +import TSCBasic +import TuistSupport +import XCTest +@testable import TuistCore +@testable import TuistSupportTesting + +final class XcodeBuildControllerCreateXCFrameworkArgumentTests: TuistUnitTestCase { + func test_xcodebuildArguments() throws { + // When: Framework + let archive = try AbsolutePath(validating: "/test.xcarchive") + let framework = "Test.framework" + XCTAssertEqual( + XcodeBuildControllerCreateXCFrameworkArgument.framework(archivePath: archive, framework: framework) + .xcodebuildArguments, + ["-archive", archive.pathString, "-framework", framework] + ) + + // When: Library + let library = try AbsolutePath(validating: "/library.a") + let headers = try AbsolutePath(validating: "/headers") + XCTAssertEqual(XcodeBuildControllerCreateXCFrameworkArgument.library( + path: library, + headers: headers + ).xcodebuildArguments, [ + "-library", + library.pathString, + "-headers", + headers.pathString, + ]) + } +} diff --git a/Tests/TuistCoreTests/ContentHashing/ContentHasherTests.swift b/Tests/TuistCoreTests/ContentHashing/ContentHasherTests.swift index 3a0ff195042..bab699cc2a5 100644 --- a/Tests/TuistCoreTests/ContentHashing/ContentHasherTests.swift +++ b/Tests/TuistCoreTests/ContentHashing/ContentHasherTests.swift @@ -118,8 +118,8 @@ final class ContentHasherTests: TuistUnitTestCase { } private func writeFiles(to folder: AbsolutePath, files: [String: String]) throws { - try files.forEach { - try mockFileHandler.write($0.value, path: folder.appending(component: $0.key), atomically: true) + for file in files { + try mockFileHandler.write(file.value, path: folder.appending(component: file.key), atomically: true) } } } diff --git a/Tests/TuistCoreTests/Graph/GraphDependencyReferenceTests.swift b/Tests/TuistCoreTests/Graph/GraphDependencyReferenceTests.swift index 844f53a0029..9638abf7b83 100644 --- a/Tests/TuistCoreTests/Graph/GraphDependencyReferenceTests.swift +++ b/Tests/TuistCoreTests/Graph/GraphDependencyReferenceTests.swift @@ -76,9 +76,12 @@ private enum KnownGraphDependencyReference: CaseIterable { case library case product case sdk + case macro func sampleReferences(name: String) -> [GraphDependencyReference] { switch self { + case .macro: + return [.testMacro(path: try! AbsolutePath(validating: "/macros/\(name)"))] case .xcframework: return [.testXCFramework(path: try! AbsolutePath(validating: "/dependencies/\(name).xcframework"))] case .framework: @@ -131,6 +134,8 @@ extension GraphDependencyReference { return .product case .sdk: return .sdk + case .macro: + return .macro } } } diff --git a/Tests/TuistCoreTests/Graph/GraphLoaderTests.swift b/Tests/TuistCoreTests/Graph/GraphLoaderTests.swift index 17c527b52ea..437a16fc912 100644 --- a/Tests/TuistCoreTests/Graph/GraphLoaderTests.swift +++ b/Tests/TuistCoreTests/Graph/GraphLoaderTests.swift @@ -368,7 +368,8 @@ final class GraphLoaderTests: TuistUnitTestCase { primaryBinaryPath: "/XCFrameworks/XF1.xcframework/ios-arm64/XF1", linking: .dynamic, mergeable: false, - status: .required + status: .required, + macroPath: nil ) ) @@ -386,12 +387,15 @@ final class GraphLoaderTests: TuistUnitTestCase { XCTAssertEqual(graph.dependencies, [ .target(name: "A", path: "/A"): Set([ .xcframework( - path: "/XCFrameworks/XF1.xcframework", - infoPlist: .test(), - primaryBinaryPath: "/XCFrameworks/XF1.xcframework/ios-arm64/XF1", - linking: .dynamic, - mergeable: false, - status: .required + GraphDependency.XCFramework( + path: "/XCFrameworks/XF1.xcframework", + infoPlist: .test(), + primaryBinaryPath: "/XCFrameworks/XF1.xcframework/ios-arm64/XF1", + linking: .dynamic, + mergeable: false, + status: .required, + macroPath: nil + ) ), ]), ]) @@ -413,7 +417,8 @@ final class GraphLoaderTests: TuistUnitTestCase { primaryBinaryPath: "/XCFrameworks/XF1.xcframework/ios-arm64/XF1", linking: .dynamic, mergeable: true, - status: .required + status: .required, + macroPath: nil ) ) @@ -431,12 +436,15 @@ final class GraphLoaderTests: TuistUnitTestCase { XCTAssertEqual(graph.dependencies, [ .target(name: "A", path: "/A"): Set([ .xcframework( - path: "/XCFrameworks/XF1.xcframework", - infoPlist: .test(), - primaryBinaryPath: "/XCFrameworks/XF1.xcframework/ios-arm64/XF1", - linking: .dynamic, - mergeable: true, - status: .required + GraphDependency.XCFramework( + path: "/XCFrameworks/XF1.xcframework", + infoPlist: .test(), + primaryBinaryPath: "/XCFrameworks/XF1.xcframework/ios-arm64/XF1", + linking: .dynamic, + mergeable: true, + status: .required, + macroPath: nil + ) ), ]), ]) diff --git a/Tests/TuistCoreTests/Graph/GraphTraverserTests.swift b/Tests/TuistCoreTests/Graph/GraphTraverserTests.swift index d349c94cc35..6fda03f8eca 100644 --- a/Tests/TuistCoreTests/Graph/GraphTraverserTests.swift +++ b/Tests/TuistCoreTests/Graph/GraphTraverserTests.swift @@ -333,8 +333,8 @@ final class GraphTraverserTests: TuistUnitTestCase { XCTAssertEqual( got, [ - GraphTarget(path: projectA.path, target: a2, project: projectA), - GraphTarget(path: projectB.path, target: b1, project: projectB), + GraphTargetReference(target: GraphTarget(path: projectA.path, target: a2, project: projectA)), + GraphTargetReference(target: GraphTarget(path: projectB.path, target: b1, project: projectB)), ] ) } @@ -1081,7 +1081,8 @@ final class GraphTraverserTests: TuistUnitTestCase { let got = subject.appClipDependencies(path: project.path, name: app.name) // Then - XCTAssertEqual(got, .init(path: project.path, target: appClip, project: project)) + let expectedTarget = GraphTarget(path: project.path, target: appClip, project: project) + XCTAssertEqual(got, GraphTargetReference(target: expectedTarget)) } func test_buildsForMacCatalyst_returns_false_when_someDependenciesCantBuildForMacCatalyst() { @@ -1177,6 +1178,44 @@ final class GraphTraverserTests: TuistUnitTestCase { XCTAssertTrue(got) } + func test_embeddableFrameworks_when_macroExecutableInBetween() throws { + /** + Target > Macro XCFramework > Macro Executable > Dynamic SwiftSyntax + + Having a macro executable that links dynamic dependencies is an scenario that Tuist might support in the future. + This test ensures that our graph traverser is accounting for that already. + */ + // Given + let target = Target.test(name: "Main", product: .app) + let precompiledMacro = GraphDependency.testXCFramework(linking: .dynamic) + let precompiledMacroExecutable = GraphDependency.testMacro() + let swiftSyntaxDynamicXCFramework = GraphDependency.testXCFramework(linking: .dynamic) + + let project = Project.test(targets: [target]) + let graphTarget = GraphDependency.target(name: target.name, path: project.path) + + // Given: Value Graph + let graph = Graph.test( + projects: [project.path: project], + targets: [project.path: [target.name: target]], + dependencies: [ + .target( + name: target.name, + path: project.path + ): Set([precompiledMacro]), + precompiledMacro: Set([swiftSyntaxDynamicXCFramework]), + precompiledMacroExecutable: Set([swiftSyntaxDynamicXCFramework]), + ] + ) + let subject = GraphTraverser(graph: graph) + + // When + let got = subject.embeddableFrameworks(path: project.path, name: target.name).sorted() + + // Then + XCTAssertEqual(got, [GraphDependencyReference(precompiledMacro)]) + } + func test_embeddableFrameworks_when_targetIsNotApp() throws { // Given let target = Target.test(name: "Main", product: .framework) @@ -1308,41 +1347,50 @@ final class GraphTraverserTests: TuistUnitTestCase { // Given: Value Graph let cDependency = GraphDependency.xcframework( - path: "/xcframeworks/c.xcframework", - infoPlist: .test(libraries: [.test( - identifier: "id", - path: try RelativePath(validating: "path"), - architectures: [.arm64] - )]), - primaryBinaryPath: "/xcframeworks/c.xcframework/c", - linking: .dynamic, - mergeable: false, - status: .required + GraphDependency.XCFramework( + path: "/xcframeworks/c.xcframework", + infoPlist: .test(libraries: [.test( + identifier: "id", + path: try RelativePath(validating: "path"), + architectures: [.arm64] + )]), + primaryBinaryPath: "/xcframeworks/c.xcframework/c", + linking: .dynamic, + mergeable: false, + status: .required, + macroPath: nil + ) ) let dDependency = GraphDependency.xcframework( - path: "/xcframeworks/d.xcframework", - infoPlist: .test(libraries: [.test( - identifier: "id", - path: try RelativePath(validating: "path"), - architectures: [.arm64] - )]), - primaryBinaryPath: "/xcframeworks/d.xcframework/d", - linking: .dynamic, - mergeable: false, - status: .required + GraphDependency.XCFramework( + path: "/xcframeworks/d.xcframework", + infoPlist: .test(libraries: [.test( + identifier: "id", + path: try RelativePath(validating: "path"), + architectures: [.arm64] + )]), + primaryBinaryPath: "/xcframeworks/d.xcframework/d", + linking: .dynamic, + mergeable: false, + status: .required, + macroPath: nil + ) ) let eDependency = GraphDependency.xcframework( - path: "/xcframeworks/e.xcframework", - infoPlist: .test(libraries: [.test( - identifier: "id", - path: try RelativePath(validating: "path"), + GraphDependency.XCFramework( + path: "/xcframeworks/e.xcframework", + infoPlist: .test(libraries: [.test( + identifier: "id", + path: try RelativePath(validating: "path"), + mergeable: true, + architectures: [.arm64] + )]), + primaryBinaryPath: "/xcframeworks/e.xcframework/e", + linking: .dynamic, mergeable: true, - architectures: [.arm64] - )]), - primaryBinaryPath: "/xcframeworks/e.xcframework/e", - linking: .dynamic, - mergeable: true, - status: .required + status: .required, + macroPath: nil + ) ) let dependencies: [GraphDependency: Set] = [ .target(name: app.name, path: project.path): Set(arrayLiteral: cDependency, eDependency), @@ -1380,41 +1428,50 @@ final class GraphTraverserTests: TuistUnitTestCase { // Given: Value Graph let cDependency = GraphDependency.xcframework( - path: "/xcframeworks/c.xcframework", - infoPlist: .test(libraries: [.test( - identifier: "id", - path: try RelativePath(validating: "c.framework"), - architectures: [.arm64] - )]), - primaryBinaryPath: "/xcframeworks/c.xcframework/c", - linking: .dynamic, - mergeable: false, - status: .required + GraphDependency.XCFramework( + path: "/xcframeworks/c.xcframework", + infoPlist: .test(libraries: [.test( + identifier: "id", + path: try RelativePath(validating: "c.framework"), + architectures: [.arm64] + )]), + primaryBinaryPath: "/xcframeworks/c.xcframework/c", + linking: .dynamic, + mergeable: false, + status: .required, + macroPath: nil + ) ) let dDependency = GraphDependency.xcframework( - path: "/xcframeworks/d.xcframework", - infoPlist: .test(libraries: [.test( - identifier: "id", - path: try RelativePath(validating: "d.framework"), - architectures: [.arm64] - )]), - primaryBinaryPath: "/xcframeworks/d.xcframework/d", - linking: .dynamic, - mergeable: false, - status: .required + GraphDependency.XCFramework( + path: "/xcframeworks/d.xcframework", + infoPlist: .test(libraries: [.test( + identifier: "id", + path: try RelativePath(validating: "d.framework"), + architectures: [.arm64] + )]), + primaryBinaryPath: "/xcframeworks/d.xcframework/d", + linking: .dynamic, + mergeable: false, + status: .required, + macroPath: nil + ) ) let eDependency = GraphDependency.xcframework( - path: "/xcframeworks/e.xcframework", - infoPlist: .test(libraries: [.test( - identifier: "id", - path: try RelativePath(validating: "e.framework"), + GraphDependency.XCFramework( + path: "/xcframeworks/e.xcframework", + infoPlist: .test(libraries: [.test( + identifier: "id", + path: try RelativePath(validating: "e.framework"), + mergeable: true, + architectures: [.arm64] + )]), + primaryBinaryPath: "/xcframeworks/e.xcframework/e", + linking: .dynamic, mergeable: true, - architectures: [.arm64] - )]), - primaryBinaryPath: "/xcframeworks/e.xcframework/e", - linking: .dynamic, - mergeable: true, - status: .required + status: .required, + macroPath: nil + ) ) let dependencies: [GraphDependency: Set] = [ .target(name: app.name, path: project.path): Set(arrayLiteral: cDependency, eDependency), @@ -1811,6 +1868,40 @@ final class GraphTraverserTests: TuistUnitTestCase { XCTAssertEqual(got, try [AbsolutePath(validating: "/test")]) } + func test_linkableDependencies_whenMacros() throws { + // Given + let target = Target.test(name: "Main", product: .app) + let macroXCFramework = GraphDependency.testXCFramework( + path: .root.appending(component: "Macro.xcframework"), + linking: .static + ) + let macroExecutable = GraphDependency.testMacro() + let swiftSyntax = GraphDependency.testXCFramework( + path: .root.appending(component: "SwiftSyntax.xcframework"), + linking: .static + ) + let project = Project.test(targets: [target]) + + // Given: Value Graph + let dependencies: [GraphDependency: Set] = [ + .target(name: target.name, path: project.path): Set([macroXCFramework]), + macroXCFramework: Set([macroExecutable]), + macroExecutable: Set([swiftSyntax]), + ] + let graph = Graph.test( + projects: [project.path: project], + targets: [project.path: [target.name: target]], + dependencies: dependencies + ) + let subject = GraphTraverser(graph: graph) + + // When + let got = try subject.linkableDependencies(path: project.path, name: target.name).sorted() + + // Then + XCTAssertEqual(got.first, GraphDependencyReference(macroXCFramework)) + } + func test_linkableDependencies_whenPrecompiled() throws { // Given let target = Target.test(name: "Main") @@ -3979,16 +4070,19 @@ final class GraphTraverserTests: TuistUnitTestCase { let project = Project.test(targets: [staticLibrary]) let directXCFramework = GraphDependency.xcframework( - path: "/xcframeworks/direct.xcframework", - infoPlist: .test(libraries: [.test( - identifier: "id", - path: try RelativePath(validating: "path"), - architectures: [.arm64] - )]), - primaryBinaryPath: "/xcframeworks/direct.xcframework/direct", - linking: .static, - mergeable: false, - status: .required + GraphDependency.XCFramework( + path: "/xcframeworks/direct.xcframework", + infoPlist: .test(libraries: [.test( + identifier: "id", + path: try RelativePath(validating: "path"), + architectures: [.arm64] + )]), + primaryBinaryPath: "/xcframeworks/direct.xcframework/direct", + linking: .static, + mergeable: false, + status: .required, + macroPath: nil + ) ) let directFramework = GraphDependency.framework( path: "/frameworks/direct.framework", @@ -4002,40 +4096,49 @@ final class GraphTraverserTests: TuistUnitTestCase { ) let directFrameworkTarget = GraphDependency.target(name: staticFramework.name, path: project.path) let transitiveFrameworkTargetXCFramework = GraphDependency.xcframework( - path: "/xcframeworks/transitive-framework-target-xcframework.xcframework", - infoPlist: .test(libraries: [.test( - identifier: "id", - path: try RelativePath(validating: "path"), - architectures: [.arm64] - )]), - primaryBinaryPath: "/xcframeworks/transitive-framework-target-xcframework.xcframework/transitive", - linking: .static, - mergeable: false, - status: .required + GraphDependency.XCFramework( + path: "/xcframeworks/transitive-framework-target-xcframework.xcframework", + infoPlist: .test(libraries: [.test( + identifier: "id", + path: try RelativePath(validating: "path"), + architectures: [.arm64] + )]), + primaryBinaryPath: "/xcframeworks/transitive-framework-target-xcframework.xcframework/transitive", + linking: .static, + mergeable: false, + status: .required, + macroPath: nil + ) ) let transitiveXCFramework = GraphDependency.xcframework( - path: "/xcframeworks/transitive.xcframework", - infoPlist: .test(libraries: [.test( - identifier: "id", - path: try RelativePath(validating: "path"), - architectures: [.arm64] - )]), - primaryBinaryPath: "/xcframeworks/transitive.xcframework/transitive", - linking: .static, - mergeable: false, - status: .required + GraphDependency.XCFramework( + path: "/xcframeworks/transitive.xcframework", + infoPlist: .test(libraries: [.test( + identifier: "id", + path: try RelativePath(validating: "path"), + architectures: [.arm64] + )]), + primaryBinaryPath: "/xcframeworks/transitive.xcframework/transitive", + linking: .static, + mergeable: false, + status: .required, + macroPath: nil + ) ) let frameworkTransitiveXCFramework = GraphDependency.xcframework( - path: "/xcframeworks/framework-transitive.xcframework", - infoPlist: .test(libraries: [.test( - identifier: "id", - path: try RelativePath(validating: "path"), - architectures: [.arm64] - )]), - primaryBinaryPath: "/xcframeworks/framework-transitive.xcframework/framework-transitive", - linking: .static, - mergeable: false, - status: .required + GraphDependency.XCFramework( + path: "/xcframeworks/framework-transitive.xcframework", + infoPlist: .test(libraries: [.test( + identifier: "id", + path: try RelativePath(validating: "path"), + architectures: [.arm64] + )]), + primaryBinaryPath: "/xcframeworks/framework-transitive.xcframework/framework-transitive", + linking: .static, + mergeable: false, + status: .required, + macroPath: nil + ) ) let dependencies: [GraphDependency: Set] = [ @@ -4500,15 +4603,34 @@ final class GraphTraverserTests: TuistUnitTestCase { XCTAssertEqual(got.sorted(), []) } - func test_directSwiftMacroFrameworkTargets_when_targetHasADirectMacroStaticFrameworkDependency() { + func test_directSwiftMacroTargets_when_targetHasADirectMacroStaticFrameworkDependency() { // Given let app = Target.test(name: "App", destinations: [.iPhone], product: .app) - let macroFramework = Target.test(name: "StaticFramework", destinations: [.iPhone], product: .staticFramework) + let staticFrameworkMacro = Target.test(name: "StaticFrameworkMacro", destinations: [.iPhone], product: .staticFramework) + let dynamicFrameworkMacro = Target.test(name: "DynamicFrameworkMacro", destinations: [.iPhone], product: .framework) + let staticLibraryMacro = Target.test(name: "StaticLibraryMacro", destinations: [.iPhone], product: .staticLibrary) + let dynamicLibraryMacro = Target.test(name: "DynamicLibraryMacro", destinations: [.iPhone], product: .dynamicLibrary) + let macro = Target.test(name: "Macro", destinations: [.mac], product: .macro) - let project = Project.test(targets: [app, macroFramework, macro]) + let project = Project.test(targets: [ + app, + staticFrameworkMacro, + dynamicFrameworkMacro, + staticLibraryMacro, + dynamicLibraryMacro, + macro, + ]) let dependencies: [GraphDependency: Set] = [ - .target(name: app.name, path: project.path): Set([.target(name: macroFramework.name, path: project.path)]), - .target(name: macroFramework.name, path: project.path): Set([.target(name: macro.name, path: project.path)]), + .target(name: app.name, path: project.path): Set([ + .target(name: staticFrameworkMacro.name, path: project.path), + .target(name: dynamicFrameworkMacro.name, path: project.path), + .target(name: staticLibraryMacro.name, path: project.path), + .target(name: dynamicLibraryMacro.name, path: project.path), + ]), + .target(name: staticFrameworkMacro.name, path: project.path): Set([.target(name: macro.name, path: project.path)]), + .target(name: dynamicFrameworkMacro.name, path: project.path): Set([.target(name: macro.name, path: project.path)]), + .target(name: staticLibraryMacro.name, path: project.path): Set([.target(name: macro.name, path: project.path)]), + .target(name: dynamicLibraryMacro.name, path: project.path): Set([.target(name: macro.name, path: project.path)]), ] // Given: Value Graph @@ -4517,7 +4639,10 @@ final class GraphTraverserTests: TuistUnitTestCase { projects: [project.path: project], targets: [project.path: [ app.name: app, - macroFramework.name: macroFramework, + staticFrameworkMacro.name: staticFrameworkMacro, + dynamicFrameworkMacro.name: dynamicFrameworkMacro, + staticLibraryMacro.name: staticLibraryMacro, + dynamicLibraryMacro.name: dynamicLibraryMacro, macro.name: macro, ]], dependencies: dependencies @@ -4525,15 +4650,30 @@ final class GraphTraverserTests: TuistUnitTestCase { let subject = GraphTraverser(graph: graph) // When - let got = subject.directSwiftMacroFrameworkTargets(path: project.path, name: app.name) + let got = subject.directSwiftMacroTargets(path: project.path, name: app.name) // Then XCTAssertEqual(got.sorted(), [ - GraphTarget(path: project.path, target: macroFramework, project: project), + GraphTargetReference( + target: GraphTarget(path: project.path, target: dynamicFrameworkMacro, project: project), + condition: nil + ), + GraphTargetReference( + target: GraphTarget(path: project.path, target: dynamicLibraryMacro, project: project), + condition: nil + ), + GraphTargetReference( + target: GraphTarget(path: project.path, target: staticFrameworkMacro, project: project), + condition: nil + ), + GraphTargetReference( + target: GraphTarget(path: project.path, target: staticLibraryMacro, project: project), + condition: nil + ), ]) } - func test_directSwiftMacroFrameworkTargets_doesntReturnAStaticFramework_when_theStaticFrameworkDoesntDependOnAMacroExecutable( + func test_directSwiftMacroTargets_doesntReturnATarget_when_theItDoesntDependOnAMacroExecutable( ) { // Given let app = Target.test(name: "App", destinations: [.iPhone], product: .app) @@ -4557,12 +4697,665 @@ final class GraphTraverserTests: TuistUnitTestCase { let subject = GraphTraverser(graph: graph) // When - let got = subject.directSwiftMacroFrameworkTargets(path: project.path, name: app.name) + let got = subject.directSwiftMacroTargets(path: project.path, name: app.name) // Then XCTAssertEqual(got.sorted(), []) } + func test_allSwiftMacroTargets_returnsTransitiveSwiftMacros() { + // Given + let app = Target.test(name: "App", destinations: [.iPhone], product: .app) + let directMacroFramework = Target.test(name: "DirectMacroFramework", destinations: [.iPhone], product: .staticFramework) + let directMacro = Target.test(name: "DirectMacro", destinations: [.mac], product: .macro) + let transitiveMacroLibrary = Target.test( + name: "TransitiveMacroLibrary", + destinations: [.iPhone], + product: .staticLibrary + ) + let transitiveMacro = Target.test(name: "TransitiveMacro", destinations: [.mac], product: .macro) + + let project = Project.test(targets: [app, directMacroFramework, directMacro]) + let dependencies: [GraphDependency: Set] = [ + .target(name: app.name, path: project.path): Set([.target(name: directMacroFramework.name, path: project.path)]), + .target(name: directMacroFramework.name, path: project.path): Set([ + .target(name: directMacro.name, path: project.path), + .target(name: transitiveMacroLibrary.name, path: project.path), + ]), + .target(name: transitiveMacroLibrary.name, path: project.path): Set([.target( + name: transitiveMacro.name, + path: project.path + )]), + ] + + // Given: Value Graph + let graph = Graph.test( + path: project.path, + projects: [project.path: project], + targets: [project.path: [ + app.name: app, + directMacroFramework.name: directMacroFramework, + directMacro.name: directMacro, + transitiveMacroLibrary.name: transitiveMacroLibrary, + transitiveMacro.name: transitiveMacro, + ]], + dependencies: dependencies + ) + let subject = GraphTraverser(graph: graph) + + // When + let got = subject.allSwiftMacroTargets(path: project.path, name: app.name) + + // Then + XCTAssertEqual(got.sorted(), [ + GraphTarget(path: project.path, target: directMacroFramework, project: project), + GraphTarget(path: project.path, target: transitiveMacroLibrary, project: project), + ]) + } + + func test_directTargetDependenciesWithConditions() throws { + // Given + let app = Target.test(name: "App", destinations: [.iPhone], product: .app) + let framework = Target.test(name: "Framework", destinations: [.iPhone], product: .framework) + let project = Project.test(targets: [app, framework]) + let appDependency = GraphDependency.target(name: app.name, path: project.path) + let frameworkDependency = GraphDependency.target(name: framework.name, path: project.path) + let dependencies: [GraphDependency: Set] = [ + appDependency: Set([frameworkDependency]), + frameworkDependency: Set([]), + ] + let platformCondition = try PlatformCondition.test([.ios]) + + // Given: Value Graph + let graph = Graph.test( + path: project.path, + projects: [project.path: project], + targets: [project.path: [ + app.name: app, + framework.name: framework, + ]], + dependencies: dependencies, + dependencyConditions: [ + GraphEdge(from: appDependency, to: frameworkDependency): platformCondition, + ] + ) + let subject = GraphTraverser(graph: graph) + + // When + let got = subject.directTargetDependencies(path: project.path, name: app.name) + + // Then + XCTAssertEqual(got.count, 1) + let result = try XCTUnwrap(got.first) + XCTAssertEqual(result.graphTarget, GraphTarget(path: project.path, target: framework, project: project)) + XCTAssertEqual(result.condition, platformCondition) + } + + // https://github.com/tuist/tuist/issues/5746 + func test_transitiveTargetDependenciesWhenIntermediateDependenciesHaveConditions() throws { + // Given + let app = Target.test(name: "App", destinations: [.iPhone, .mac], product: .app) + let frameworkA = Target.test(name: "FrameworkA", destinations: [.iPhone, .mac], product: .framework) + let frameworkB = Target.test(name: "FrameworkB", destinations: [.iPhone], product: .framework) + let frameworkC = Target.test(name: "FrameworkC", destinations: [.iPhone, .mac], product: .framework) + let frameworkD = Target.test(name: "FrameworkD", destinations: [.iPhone, .mac], product: .framework) + + let project = Project.test(targets: [app, frameworkA, frameworkB, frameworkC, frameworkD]) + let appDependency = GraphDependency.target(name: app.name, path: project.path) + let frameworkADependency = GraphDependency.target(name: frameworkA.name, path: project.path) + let frameworkBDependency = GraphDependency.target(name: frameworkB.name, path: project.path) + let frameworkCDependency = GraphDependency.target(name: frameworkC.name, path: project.path) + let frameworkDDependency = GraphDependency.target(name: frameworkD.name, path: project.path) + + let dependencies: [GraphDependency: Set] = [ + appDependency: Set([ + frameworkADependency, + frameworkBDependency, + ]), + frameworkADependency: Set([frameworkCDependency]), + frameworkBDependency: Set([frameworkCDependency]), + frameworkCDependency: Set([frameworkDDependency]), + ] + let platformCondition = try PlatformCondition.test([.ios]) + + // Given: Value Graph + let graph = Graph.test( + path: project.path, + projects: [project.path: project], + targets: [project.path: [ + app.name: app, + frameworkA.name: frameworkA, + frameworkB.name: frameworkB, + frameworkC.name: frameworkC, + frameworkD.name: frameworkD, + ]], + dependencies: dependencies, + dependencyConditions: [ + GraphEdge(from: frameworkBDependency, to: frameworkCDependency): platformCondition, + ] + ) + + for _ in 0 ..< 50 { + let subject = GraphTraverser(graph: graph) + + // When + let appToFrameworkC = subject.combinedCondition( + to: frameworkCDependency, + from: appDependency + ) + + let appToFrameworkD = subject.combinedCondition( + to: frameworkDDependency, + from: appDependency + ) + + // Then + XCTAssertEqual(appToFrameworkC, .condition(nil)) + XCTAssertEqual(appToFrameworkD, .condition(nil)) + } + } + + func test_orphanExternalDependencies() throws { + // Given + let app = Target.test(name: "App", destinations: [.iPhone], product: .app) + let project = Project.test(path: try! AbsolutePath(validating: "/App"), targets: [app]) + let appDependency = GraphDependency.target(name: app.name, path: project.path) + let directPackageProduct = Target.test(name: "DirectPackage", destinations: [.iPhone], product: .app) + let transitivePackageProduct = Target.test(name: "TransitivePackage", destinations: [.iPhone], product: .app) + let packageDevProduct = Target.test(name: "DevPackage", destinations: [.iPhone], product: .app) + let packageProject = Project.test( + path: try! AbsolutePath(validating: "/Package"), + name: "Package", + targets: [directPackageProduct, transitivePackageProduct, packageDevProduct], + isExternal: true + ) + let directPackageProductDependency = GraphDependency.target(name: directPackageProduct.name, path: packageProject.path) + let transitivePackageProductDependency = GraphDependency.target( + name: transitivePackageProduct.name, + path: packageProject.path + ) + + let graph = Graph.test( + path: project.path, + projects: [project.path: project, packageProject.path: packageProject], + targets: [project.path: [ + app.name: app, + ], packageProject.path: [ + directPackageProduct.name: directPackageProduct, + transitivePackageProduct.name: transitivePackageProduct, + packageDevProduct.name: packageDevProduct, + ]], + dependencies: [ + appDependency: Set([directPackageProductDependency]), + directPackageProductDependency: Set([transitivePackageProductDependency]), + ] + ) + + // When + let got = GraphTraverser(graph: graph).allOrphanExternalTargets() + + // Then + XCTAssertEqual(got, Set([GraphTarget(path: packageProject.path, target: packageDevProduct, project: packageProject)])) + } + + func test_targetsWithExternalDependencies() { + // Given + let app = Target.test(name: "App", destinations: [.iPhone], product: .app) + let framework = Target.test(name: "Framework", destinations: [.iPhone], product: .framework) + let project = Project.test(path: try! AbsolutePath(validating: "/App"), targets: [app, framework]) + let appDependency = GraphDependency.target(name: app.name, path: project.path) + let frameworkDependency = GraphDependency.target(name: framework.name, path: project.path) + + let directPackageProduct = Target.test(name: "DirectPackage", destinations: [.iPhone], product: .app) + let packageProject = Project.test( + path: try! AbsolutePath(validating: "/Package"), + name: "Package", + targets: [directPackageProduct], + isExternal: true + ) + let directPackageProductDependency = GraphDependency.target(name: directPackageProduct.name, path: packageProject.path) + + let graph = Graph.test( + path: project.path, + projects: [project.path: project, packageProject.path: packageProject], + targets: [project.path: [ + app.name: app, + framework.name: framework, + ], packageProject.path: [ + directPackageProduct.name: directPackageProduct, + ]], + dependencies: [ + appDependency: Set([frameworkDependency]), + frameworkDependency: Set([directPackageProductDependency]), + ] + ) + + // When + let got = GraphTraverser(graph: graph).targetsWithExternalDependencies() + + // Then + XCTAssertEqual(got, Set([GraphTarget(path: project.path, target: framework, project: project)])) + } + + func test_allExternalTargets() { + // Given + let app = Target.test(name: "App", destinations: [.iPhone], product: .app) + let framework = Target.test(name: "Framework", destinations: [.iPhone], product: .framework) + let project = Project.test(path: try! AbsolutePath(validating: "/App"), targets: [app, framework]) + let appDependency = GraphDependency.target(name: app.name, path: project.path) + let frameworkDependency = GraphDependency.target(name: framework.name, path: project.path) + + let directPackageProduct = Target.test(name: "DirectPackage", destinations: [.iPhone], product: .app) + let packageProject = Project.test( + path: try! AbsolutePath(validating: "/Package"), + name: "Package", + targets: [directPackageProduct], + isExternal: true + ) + let directPackageProductDependency = GraphDependency.target(name: directPackageProduct.name, path: packageProject.path) + + let graph = Graph.test( + path: project.path, + projects: [project.path: project, packageProject.path: packageProject], + targets: [project.path: [ + app.name: app, + framework.name: framework, + ], packageProject.path: [ + directPackageProduct.name: directPackageProduct, + ]], + dependencies: [ + appDependency: Set([frameworkDependency]), + frameworkDependency: Set([directPackageProductDependency]), + ] + ) + + // When + let got = GraphTraverser(graph: graph).allExternalTargets() + + // Then + XCTAssertEqual(got, Set([GraphTarget(path: packageProject.path, target: directPackageProduct, project: packageProject)])) + } + + func test_externalTargetSupportedPlatforms_when_external_dependency_without_platform_filter() async throws { + // Given + let directory = try temporaryPath() + let packagesDirectory = directory.appending(component: "Dependencies") + + let appTarget = Target.test(name: "App", destinations: [.iPad, .iPhone]) + let externalPackage = Target.test( + name: "Package", + destinations: [.iPad, .iPhone, .appleWatch, .appleTv, .mac], + product: .framework + ) + + let project = Project.test(path: directory, targets: [appTarget]) + let externalProject = Project.test(path: packagesDirectory, targets: [externalPackage], isExternal: true) + + let appTargetDependency = GraphDependency.target(name: appTarget.name, path: project.path) + let externalPackageDependency = GraphDependency.target(name: externalPackage.name, path: externalProject.path) + + let graph = Graph.test( + projects: [ + directory: project, + packagesDirectory: externalProject, + ], + targets: [ + project.path: [ + appTarget.name: appTarget, + ], + externalProject.path: [ + externalPackage.name: externalPackage, + ], + ], + dependencies: [ + appTargetDependency: Set([externalPackageDependency]), + ] + ) + + // When + let got = GraphTraverser(graph: graph).externalTargetSupportedPlatforms() + + // Then + XCTAssertNil(got[GraphTarget(path: project.path, target: appTarget, project: project)]) + XCTAssertEqual( + got[GraphTarget(path: externalProject.path, target: externalPackage, project: externalProject)], + Set([.iOS]) + ) + } + + func test_test_externalTargetSupportedPlatforms_when_external_transitive_dependency_without_platform_filter() async throws { + // Given + let directory = try temporaryPath() + let packagesDirectory = directory.appending(component: "Dependencies") + + let appTarget = Target.test(name: "App", destinations: [.iPad, .iPhone]) + let directExternalPackage = Target.test( + name: "Direct", + destinations: [.iPad, .iPhone], + product: .framework + ) + let transitiveExternalPackage = Target.test( + name: "Transitive", + destinations: [.iPad, .iPhone, .appleWatch, .appleTv, .mac, .macWithiPadDesign, .macCatalyst], + product: .framework + ) + + let project = Project.test(path: directory, targets: [appTarget]) + let externalProject = Project.test( + path: packagesDirectory, + targets: [directExternalPackage, transitiveExternalPackage], + isExternal: true + ) + + let appTargetDependency = GraphDependency.target(name: appTarget.name, path: project.path) + let directExternalPackageDependency = GraphDependency.target(name: directExternalPackage.name, path: externalProject.path) + let transitiveExternalPackageDependency = GraphDependency.target( + name: transitiveExternalPackage.name, + path: externalProject.path + ) + + let graph = Graph.test( + projects: [ + directory: project, + packagesDirectory: externalProject, + ], + targets: [ + project.path: [ + appTarget.name: appTarget, + ], + externalProject.path: [ + directExternalPackage.name: directExternalPackage, + transitiveExternalPackage.name: transitiveExternalPackage, + ], + ], + dependencies: [ + appTargetDependency: Set([directExternalPackageDependency]), + directExternalPackageDependency: Set([transitiveExternalPackageDependency]), + ] + ) + + // When + let got = GraphTraverser(graph: graph).externalTargetSupportedPlatforms() + + // Then + XCTAssertNil(got[GraphTarget(path: project.path, target: appTarget, project: project)]) + XCTAssertEqual( + got[GraphTarget(path: externalProject.path, target: directExternalPackage, project: externalProject)], + Set([.iOS]) + ) + XCTAssertEqual( + got[GraphTarget(path: externalProject.path, target: transitiveExternalPackage, project: externalProject)], + Set([.iOS]) + ) + } + + func test_externalTargetSupportedPlatforms_when_external_macro_dependency() async throws { + // Given + let directory = try temporaryPath() + let packagesDirectory = directory.appending(component: "Dependencies") + + let appTarget = Target.test(name: "App", destinations: [.iPad, .iPhone]) + let externalMacroFramework = Target.test( + name: "MacroFramework", + destinations: [.iPad, .iPhone], + product: .staticFramework + ) + let externalMacroExecutable = Target.test(name: "MacroExcutable", destinations: [.mac], product: .macro) + + let project = Project.test(path: directory, targets: [appTarget]) + let externalProject = Project.test( + path: packagesDirectory, + targets: [externalMacroFramework, externalMacroExecutable], + isExternal: true + ) + + let appTargetDependency = GraphDependency.target(name: appTarget.name, path: project.path) + let externalMacroFrameworkDependency = GraphDependency.target( + name: externalMacroFramework.name, + path: externalProject.path + ) + let externalMacroExecutableDependency = GraphDependency.target( + name: externalMacroExecutable.name, + path: externalProject.path + ) + + let graph = Graph.test( + projects: [ + directory: project, + packagesDirectory: externalProject, + ], + targets: [ + project.path: [ + appTarget.name: appTarget, + ], + externalProject.path: [ + externalMacroFramework.name: externalMacroFramework, + externalMacroExecutable.name: externalMacroExecutable, + ], + ], + dependencies: [ + appTargetDependency: Set([externalMacroFrameworkDependency]), + externalMacroFrameworkDependency: Set([externalMacroExecutableDependency]), + ] + ) + + // When + let got = GraphTraverser(graph: graph).externalTargetSupportedPlatforms() + + // Then + XCTAssertNil(got[GraphTarget(path: project.path, target: appTarget, project: project)]) + XCTAssertEqual( + got[GraphTarget(path: externalProject.path, target: externalMacroFramework, project: externalProject)], + Set([.iOS]) + ) + XCTAssertEqual( + got[GraphTarget(path: externalProject.path, target: externalMacroExecutable, project: externalProject)], + Set([.macOS]) + ) + } + + func test_directTargetExternalDependencies() throws { + // Given + let directory = try temporaryPath() + let packagesDirectory = directory.appending(component: "Dependencies") + + let appTarget = Target.test(name: "App", destinations: [.iPad, .iPhone]) + let externalFramework = Target.test( + name: "Framework", + destinations: [.iPad, .iPhone], + product: .staticFramework + ) + + let project = Project.test(path: directory, targets: [appTarget]) + let externalProject = Project.test( + path: packagesDirectory, + targets: [externalFramework], + isExternal: true + ) + + let appTargetDependency = GraphDependency.target(name: appTarget.name, path: project.path) + let externalFrameworkDependency = GraphDependency.target( + name: externalFramework.name, + path: externalProject.path + ) + + let graph = Graph.test( + projects: [ + directory: project, + packagesDirectory: externalProject, + ], + targets: [ + project.path: [ + appTarget.name: appTarget, + ], + externalProject.path: [ + externalFramework.name: externalFramework, + ], + ], + dependencies: [ + appTargetDependency: Set([externalFrameworkDependency]), + ] + ) + + // When + let got = GraphTraverser(graph: graph).directTargetExternalDependencies(path: project.path, name: appTarget.name) + + // Then + XCTAssertEqual(got, Set([ + GraphTargetReference(target: GraphTarget( + path: externalProject.path, + target: externalFramework, + project: externalProject + )), + ])) + } + + func test_allSwiftPluginExecutables_includesAllXCFrameworkMacros_when_theyAreDirectOrTransitiveDependencies() throws { + // Given + let directory = try temporaryPath() + let appTarget = Target.test(name: "App", destinations: [.appleWatch]) + let project = Project.test(path: directory, targets: [appTarget]) + let appTargetDependency = GraphDependency.target(name: appTarget.name, path: project.path) + let precompiledMacroXCFramework = GraphDependency.testXCFramework() + let macroPath = AbsolutePath.root.appending(components: ["macros", "macro.macro"]) + let precompiledMacroExecutable = GraphDependency.testMacro(path: macroPath) + + let graph = Graph.test( + projects: [ + directory: project, + ], + targets: [ + project.path: [ + appTarget.name: appTarget, + ], + ], + dependencies: [ + appTargetDependency: Set([precompiledMacroXCFramework]), + precompiledMacroXCFramework: Set([precompiledMacroExecutable]), + ] + ) + + // When + let got = GraphTraverser(graph: graph).allSwiftPluginExecutables(path: project.path, name: appTarget.name) + + XCTAssertEqual(got.sorted(), [ + "\(macroPath.pathString)#\(macroPath.basename.replacingOccurrences(of: ".macro", with: ""))", + ]) + } + + func test_allSwiftPluginExecutables_staticFrameworksThatDependOnMacroTargets_when_theyAreDirectOrTransitiveDependencies( + ) throws { + // Given + let directory = try temporaryPath() + let appTarget = Target.test(name: "App", destinations: [.appleWatch]) + let directMacroStaticFrameworkTarget = Target.test( + name: "DirectMacroStaticFramework", + destinations: [.appleWatch], + product: .staticFramework + ) + let directMacroMacroTarget = Target.test(name: "DirectMacro", destinations: [.appleWatch], product: .macro) + let transitiveMacroStaticFrameworkTarget = Target.test( + name: "TransitiveMacroStaticFramework", + destinations: [.appleWatch], + product: .staticFramework + ) + let transitiveMacroMacroTarget = Target.test(name: "TransitiveMacro", destinations: [.appleWatch], product: .macro) + + let project = Project.test(path: directory, targets: [appTarget]) + let appTargetDependency = GraphDependency.target(name: appTarget.name, path: project.path) + let directMacroStaticFrameworkTargetDependency = GraphDependency.target( + name: directMacroStaticFrameworkTarget.name, + path: project.path + ) + let directMacroMacroTargetDependency = GraphDependency.target(name: directMacroMacroTarget.name, path: project.path) + let transitiveMacroStaticFrameworkTargetDependency = GraphDependency.target( + name: transitiveMacroStaticFrameworkTarget.name, + path: project.path + ) + let transitiveMacroMacroTargetDependency = GraphDependency.target( + name: transitiveMacroMacroTarget.name, + path: project.path + ) + + let graph = Graph.test( + projects: [ + directory: project, + ], + targets: [ + project.path: [ + appTarget.name: appTarget, + directMacroStaticFrameworkTarget.name: directMacroStaticFrameworkTarget, + directMacroMacroTarget.name: directMacroMacroTarget, + transitiveMacroStaticFrameworkTarget.name: transitiveMacroStaticFrameworkTarget, + transitiveMacroMacroTarget.name: transitiveMacroMacroTarget, + ], + ], + dependencies: [ + appTargetDependency: Set([directMacroStaticFrameworkTargetDependency]), + directMacroStaticFrameworkTargetDependency: Set([ + directMacroMacroTargetDependency, + transitiveMacroStaticFrameworkTargetDependency, + ]), + transitiveMacroStaticFrameworkTargetDependency: Set([transitiveMacroMacroTargetDependency]), + ] + ) + + // When + let got = GraphTraverser(graph: graph).allSwiftPluginExecutables(path: project.path, name: appTarget.name) + + XCTAssertEqual(got.sorted(), [ + "$BUILD_DIR/Debug$EFFECTIVE_PLATFORM_NAME/DirectMacro#DirectMacro", + "$BUILD_DIR/Debug$EFFECTIVE_PLATFORM_NAME/TransitiveMacro#TransitiveMacro", + ]) + } + + func test_allSwiftPluginExecutables_when_staticMacroFrameworkThatDependOnMacroPrecompiledExecutable( + ) throws { + // Given + let directory = try temporaryPath() + let appTarget = Target.test(name: "App", destinations: [.appleWatch]) + let directMacroStaticFrameworkTarget = Target.test( + name: "DirectMacroStaticFramework", + destinations: [.appleWatch], + product: .staticFramework + ) + let precompiledMacroPath: AbsolutePath = .root.appending(component: "macro.macro") + let directMacroMacroPrecompiledExecutable = GraphDependency.macro(path: precompiledMacroPath) + + let project = Project.test(path: directory, targets: [appTarget]) + let appTargetDependency = GraphDependency.target(name: appTarget.name, path: project.path) + let directMacroStaticFrameworkTargetDependency = GraphDependency.target( + name: directMacroStaticFrameworkTarget.name, + path: project.path + ) + + let graph = Graph.test( + projects: [ + directory: project, + ], + targets: [ + project.path: [ + appTarget.name: appTarget, + directMacroStaticFrameworkTarget.name: directMacroStaticFrameworkTarget, + ], + ], + dependencies: [ + appTargetDependency: Set([directMacroStaticFrameworkTargetDependency]), + directMacroStaticFrameworkTargetDependency: Set([ + directMacroMacroPrecompiledExecutable, + ]), + ] + ) + + // When + let got = GraphTraverser(graph: graph).allSwiftPluginExecutables(path: project.path, name: appTarget.name) + + // Then + XCTAssertEqual(got.sorted(), [ + "\(precompiledMacroPath.pathString)#\(precompiledMacroPath.basename.replacingOccurrences(of: ".macro", with: ""))", + ]) + } + // MARK: - Helpers private func sdkDependency(from dependency: GraphDependencyReference) -> SDKPathAndStatus? { diff --git a/Tests/TuistCoreTests/MetadataProviders/XCFrameworkMetadataProviderTests.swift b/Tests/TuistCoreTests/MetadataProviders/XCFrameworkMetadataProviderTests.swift index 717a8b9225d..80343dce1dd 100644 --- a/Tests/TuistCoreTests/MetadataProviders/XCFrameworkMetadataProviderTests.swift +++ b/Tests/TuistCoreTests/MetadataProviders/XCFrameworkMetadataProviderTests.swift @@ -135,7 +135,8 @@ final class XCFrameworkMetadataProviderTests: TuistTestCase { primaryBinaryPath: expectedBinaryPath, linking: .dynamic, mergeable: false, - status: .required + status: .required, + macroPath: nil )) } @@ -170,7 +171,8 @@ final class XCFrameworkMetadataProviderTests: TuistTestCase { primaryBinaryPath: expectedBinaryPath, linking: .dynamic, mergeable: true, - status: .required + status: .required, + macroPath: nil )) } @@ -204,7 +206,8 @@ final class XCFrameworkMetadataProviderTests: TuistTestCase { primaryBinaryPath: expectedBinaryPath, linking: .static, mergeable: false, - status: .required + status: .required, + macroPath: nil )) } @@ -238,11 +241,35 @@ final class XCFrameworkMetadataProviderTests: TuistTestCase { primaryBinaryPath: expectedBinaryPath, linking: .dynamic, mergeable: false, - status: .required + status: .required, + macroPath: nil )) XCTAssertPrinterOutputContains(""" MyFrameworkMissingArch.xcframework is missing architecture ios-x86_64-simulator/MyFrameworkMissingArch.framework/MyFrameworkMissingArch defined in the Info.plist """) } + + func test_loadMetadata_when_containsMacros() throws { + // Given + let temporaryDirectory = try temporaryPath() + let xcframeworkPath = temporaryDirectory.appending(component: "MyFramework.xcframework") + try fileHandler.copy( + from: fixturePath(path: try RelativePath(validating: "MyFramework.xcframework")), + to: xcframeworkPath + ) + var macroPaths: [AbsolutePath] = [] + for frameworkPath in fileHandler.glob(xcframeworkPath, glob: "*/*.framework").sorted() { + try fileHandler.createFolder(frameworkPath.appending(component: "Macros")) + let macroPath = frameworkPath.appending(components: ["Macros", "MyFramework"]) + try fileHandler.touch(macroPath) + macroPaths.append(macroPath) + } + + // When + let metadata = try subject.loadMetadata(at: xcframeworkPath, status: .required) + + // Then + XCTAssertEqual(metadata.macroPath, macroPaths.sorted().first) + } } diff --git a/Tests/TuistCoreTests/NodeLoaders/XCFrameworkLoaderTests.swift b/Tests/TuistCoreTests/NodeLoaders/XCFrameworkLoaderTests.swift index e5d99b588c4..5bfa725f15d 100644 --- a/Tests/TuistCoreTests/NodeLoaders/XCFrameworkLoaderTests.swift +++ b/Tests/TuistCoreTests/NodeLoaders/XCFrameworkLoaderTests.swift @@ -70,7 +70,8 @@ final class XCFrameworkLoaderTests: TuistUnitTestCase { primaryBinaryPath: binaryPath, linking: linking, mergeable: false, - status: .required + status: .required, + macroPath: nil ) } @@ -81,12 +82,15 @@ final class XCFrameworkLoaderTests: TuistUnitTestCase { XCTAssertEqual( got, .xcframework( - path: xcframeworkPath, - infoPlist: infoPlist, - primaryBinaryPath: binaryPath, - linking: linking, - mergeable: false, - status: .required + GraphDependency.XCFramework( + path: xcframeworkPath, + infoPlist: infoPlist, + primaryBinaryPath: binaryPath, + linking: linking, + mergeable: false, + status: .required, + macroPath: nil + ) ) ) } diff --git a/Tests/TuistCoreTests/Utils/GraphCircularDetectorTests.swift b/Tests/TuistCoreTests/Utils/GraphCircularDetectorTests.swift index 26515f96fbf..c10cf9207ba 100644 --- a/Tests/TuistCoreTests/Utils/GraphCircularDetectorTests.swift +++ b/Tests/TuistCoreTests/Utils/GraphCircularDetectorTests.swift @@ -179,8 +179,8 @@ final class GraphCircularDetectorTests: XCTestCase { var nodes = (0 ..< 1000).map { node("\($0)") }.shuffled() while let node = nodes.popLast() { - nodes.forEach { - subject.start(from: $0, to: node) + for item in nodes { + subject.start(from: item, to: node) } } diff --git a/Tests/TuistDependenciesAcceptanceTests/DependenciesAcceptanceTests.swift b/Tests/TuistDependenciesAcceptanceTests/DependenciesAcceptanceTests.swift new file mode 100644 index 00000000000..a06f854e4f6 --- /dev/null +++ b/Tests/TuistDependenciesAcceptanceTests/DependenciesAcceptanceTests.swift @@ -0,0 +1,15 @@ +import TSCBasic +import TuistAcceptanceTesting +import TuistSupport +import TuistSupportTesting +import XcodeProj +import XCTest + +final class DependenciesAcceptanceTestAppWithSPMDependencies: TuistAcceptanceTestCase { + func test_app_spm_dependencies() async throws { + try setUpFixture(.appWithSpmDependencies) + try await run(FetchCommand.self) + try await run(GenerateCommand.self) + try await run(BuildCommand.self, "App") + } +} diff --git a/Tests/TuistDependenciesTests/Mappers/ExternalProjectsPlatformNarrowerGraphMapperTests.swift b/Tests/TuistDependenciesTests/Mappers/ExternalProjectsPlatformNarrowerGraphMapperTests.swift new file mode 100644 index 00000000000..aebb4b81d2f --- /dev/null +++ b/Tests/TuistDependenciesTests/Mappers/ExternalProjectsPlatformNarrowerGraphMapperTests.swift @@ -0,0 +1,261 @@ +import Foundation +import TuistGraph +import TuistGraphTesting +import XCTest + +@testable import TuistDependencies +@testable import TuistDependenciesTesting +@testable import TuistSupportTesting + +final class ExternalProjectsPlatformNarrowerGraphMapperTests: TuistUnitTestCase { + var subject: ExternalProjectsPlatformNarrowerGraphMapper! + + override func setUp() { + super.setUp() + subject = ExternalProjectsPlatformNarrowerGraphMapper() + } + + override func tearDown() { + subject = nil + super.tearDown() + } + + func test_map_when_external_dependency_without_platform_filter() async throws { + // Given + let directory = try temporaryPath() + let packagesDirectory = directory.appending(component: "Dependencies") + + let appTarget = Target.test(name: "App", destinations: [.iPad, .iPhone]) + let externalPackage = Target.test( + name: "Package", + destinations: [.iPad, .iPhone, .appleWatch, .appleTv, .mac], + product: .framework + ) + + let project = Project.test(path: directory, targets: [appTarget]) + let externalProject = Project.test(path: packagesDirectory, targets: [externalPackage], isExternal: true) + + let appTargetDependency = GraphDependency.target(name: appTarget.name, path: project.path) + let externalPackageDependency = GraphDependency.target(name: externalPackage.name, path: externalProject.path) + + let graph = Graph.test( + projects: [ + directory: project, + packagesDirectory: externalProject, + ], + targets: [ + project.path: [ + appTarget.name: appTarget, + ], + externalProject.path: [ + externalPackage.name: externalPackage, + ], + ], + dependencies: [ + appTargetDependency: Set([externalPackageDependency]), + ] + ) + + // When + let (mappedGraph, _) = try await subject.map(graph: graph) + + // Then + XCTAssertEqual(try XCTUnwrap(mappedGraph.targets[project.path]?[appTarget.name]?.supportedPlatforms), Set([.iOS])) + XCTAssertEqual( + try XCTUnwrap(mappedGraph.targets[externalProject.path]![externalPackage.name]?.supportedPlatforms), + Set([.iOS]) + ) + } + + func test_map_when_external_with_platform_filter() async throws { + // Given + let directory = try temporaryPath() + let packagesDirectory = directory.appending(component: "Dependencies") + + let appTarget = Target.test(name: "App", destinations: [.iPad, .iPhone, .appleWatch, .appleTv, .mac]) + let externalPackage = Target.test( + name: "Package", + destinations: [.iPhone, .iPad, .appleWatch], + product: .framework, + deploymentTargets: .init(iOS: "16.0", macOS: nil, watchOS: "9.0", tvOS: nil, visionOS: nil) + ) + + let project = Project.test(path: directory, targets: [appTarget]) + let externalProject = Project.test(path: packagesDirectory, targets: [externalPackage], isExternal: true) + + let appTargetDependency = GraphDependency.target(name: appTarget.name, path: project.path) + let externalPackageDependency = GraphDependency.target(name: externalPackage.name, path: externalProject.path) + + // Only use the external target on iOS + let dependencyCondition = try XCTUnwrap(PlatformCondition.when([.ios])) + + let graph = Graph.test( + projects: [ + directory: project, + packagesDirectory: externalProject, + ], + targets: [ + project.path: [ + appTarget.name: appTarget, + ], + externalProject.path: [ + externalPackage.name: externalPackage, + ], + ], + dependencies: [ + appTargetDependency: Set([externalPackageDependency]), + ], + dependencyConditions: [ + GraphEdge(from: appTargetDependency, to: externalPackageDependency): dependencyCondition, + ] + ) + + // When + let (mappedGraph, _) = try await subject.map(graph: graph) + + // Then + XCTAssertEqual( + try XCTUnwrap(mappedGraph.targets[project.path]?[appTarget.name]?.supportedPlatforms), + Set([.iOS, .macOS, .tvOS, .watchOS]) + ) + XCTAssertEqual( + try XCTUnwrap(mappedGraph.targets[externalProject.path]![externalPackage.name]?.supportedPlatforms), + Set([.iOS]) + ) + XCTAssertEqual( + try XCTUnwrap(mappedGraph.targets[externalProject.path]![externalPackage.name]?.deploymentTargets), + .iOS("16.0") + ) + } + + func test_map_when_external_transitive_dependency_without_platform_filter() async throws { + // Given + let directory = try temporaryPath() + let packagesDirectory = directory.appending(component: "Dependencies") + + let appTarget = Target.test(name: "App", destinations: [.iPad, .iPhone]) + let directExternalPackage = Target.test( + name: "Direct", + destinations: [.iPad, .iPhone], + product: .framework + ) + let transitiveExternalPackage = Target.test( + name: "Transitive", + destinations: [.iPad, .iPhone, .appleWatch, .appleTv, .mac, .macWithiPadDesign, .macCatalyst], + product: .framework + ) + + let project = Project.test(path: directory, targets: [appTarget]) + let externalProject = Project.test( + path: packagesDirectory, + targets: [directExternalPackage, transitiveExternalPackage], + isExternal: true + ) + + let appTargetDependency = GraphDependency.target(name: appTarget.name, path: project.path) + let directExternalPackageDependency = GraphDependency.target(name: directExternalPackage.name, path: externalProject.path) + let transitiveExternalPackageDependency = GraphDependency.target( + name: transitiveExternalPackage.name, + path: externalProject.path + ) + + let graph = Graph.test( + projects: [ + directory: project, + packagesDirectory: externalProject, + ], + targets: [ + project.path: [ + appTarget.name: appTarget, + ], + externalProject.path: [ + directExternalPackage.name: directExternalPackage, + transitiveExternalPackage.name: transitiveExternalPackage, + ], + ], + dependencies: [ + appTargetDependency: Set([directExternalPackageDependency]), + directExternalPackageDependency: Set([transitiveExternalPackageDependency]), + ] + ) + + // When + let (mappedGraph, _) = try await subject.map(graph: graph) + + // Then + XCTAssertEqual(try XCTUnwrap(mappedGraph.targets[project.path]?[appTarget.name]?.supportedPlatforms), Set([.iOS])) + XCTAssertEqual( + try XCTUnwrap(mappedGraph.targets[externalProject.path]?[directExternalPackage.name]?.supportedPlatforms), + Set([.iOS]) + ) + XCTAssertEqual( + try XCTUnwrap(mappedGraph.targets[externalProject.path]?[transitiveExternalPackage.name]?.supportedPlatforms), + Set([.iOS]) + ) + } + + func test_map_when_external_macro_dependency() async throws { + // Given + let directory = try temporaryPath() + let packagesDirectory = directory.appending(component: "Dependencies") + + let appTarget = Target.test(name: "App", destinations: [.iPad, .iPhone]) + let externalMacroFramework = Target.test( + name: "MacroFramework", + destinations: [.iPad, .iPhone], + product: .staticFramework + ) + let externalMacroExecutable = Target.test(name: "MacroExcutable", destinations: [.mac], product: .macro) + + let project = Project.test(path: directory, targets: [appTarget]) + let externalProject = Project.test( + path: packagesDirectory, + targets: [externalMacroFramework, externalMacroExecutable], + isExternal: true + ) + + let appTargetDependency = GraphDependency.target(name: appTarget.name, path: project.path) + let externalMacroFrameworkDependency = GraphDependency.target( + name: externalMacroFramework.name, + path: externalProject.path + ) + let externalMacroExecutableDependency = GraphDependency.target( + name: externalMacroExecutable.name, + path: externalProject.path + ) + + let graph = Graph.test( + projects: [ + directory: project, + packagesDirectory: externalProject, + ], + targets: [ + project.path: [ + appTarget.name: appTarget, + ], + externalProject.path: [ + externalMacroFramework.name: externalMacroFramework, + externalMacroExecutable.name: externalMacroExecutable, + ], + ], + dependencies: [ + appTargetDependency: Set([externalMacroFrameworkDependency]), + externalMacroFrameworkDependency: Set([externalMacroExecutableDependency]), + ] + ) + + // When + let (mappedGraph, _) = try await subject.map(graph: graph) + + // Then + XCTAssertEqual(try XCTUnwrap(mappedGraph.targets[project.path]?[appTarget.name]?.supportedPlatforms), Set([.iOS])) + XCTAssertEqual( + try XCTUnwrap(mappedGraph.targets[externalProject.path]?[externalMacroFramework.name]?.supportedPlatforms), + Set([.iOS]) + ) + XCTAssertEqual( + try XCTUnwrap(mappedGraph.targets[externalProject.path]?[externalMacroExecutable.name]?.supportedPlatforms), + Set([.macOS]) + ) + } +} diff --git a/Tests/TuistDependenciesTests/Mappers/PruneOrphanExternalTargetsGraphMapperTests.swift b/Tests/TuistDependenciesTests/Mappers/PruneOrphanExternalTargetsGraphMapperTests.swift new file mode 100644 index 00000000000..4b61b5d4bce --- /dev/null +++ b/Tests/TuistDependenciesTests/Mappers/PruneOrphanExternalTargetsGraphMapperTests.swift @@ -0,0 +1,69 @@ +import Foundation +import TSCBasic +import TuistGraph +import TuistGraphTesting +import XCTest + +@testable import TuistDependencies +@testable import TuistDependenciesTesting +@testable import TuistSupportTesting + +final class PruneOrphanExternalTargetsGraphMapperTests: TuistUnitTestCase { + var subject: PruneOrphanExternalTargetsGraphMapper! + + override func setUp() { + super.setUp() + subject = PruneOrphanExternalTargetsGraphMapper() + } + + override func tearDown() { + subject = nil + super.tearDown() + } + + func test_map_when_external_targets_to_prune() async throws { + // Given + let app = Target.test(name: "App", destinations: [.iPhone], product: .app) + let project = Project.test(path: try! AbsolutePath(validating: "/App"), targets: [app]) + let appDependency = GraphDependency.target(name: app.name, path: project.path) + let directPackageProduct = Target.test(name: "DirectPackage", destinations: [.iPhone], product: .app) + let transitivePackageProduct = Target.test(name: "TransitivePackage", destinations: [.iPhone], product: .app) + let packageDevProduct = Target.test(name: "DevPackage", destinations: [.iPhone], product: .app) + let packageProject = Project.test( + path: try! AbsolutePath(validating: "/Package"), + name: "Package", + targets: [directPackageProduct, transitivePackageProduct, packageDevProduct], + isExternal: true + ) + let directPackageProductDependency = GraphDependency.target(name: directPackageProduct.name, path: packageProject.path) + let transitivePackageProductDependency = GraphDependency.target( + name: transitivePackageProduct.name, + path: packageProject.path + ) + + let graph = Graph.test( + path: project.path, + projects: [project.path: project, packageProject.path: packageProject], + targets: [project.path: [ + app.name: app, + ], packageProject.path: [ + directPackageProduct.name: directPackageProduct, + transitivePackageProduct.name: transitivePackageProduct, + packageDevProduct.name: packageDevProduct, + ]], + dependencies: [ + appDependency: Set([directPackageProductDependency]), + directPackageProductDependency: Set([transitivePackageProductDependency]), + ] + ) + + // When + let (gotGraph, _) = try await subject.map(graph: graph) + + // Then + XCTAssertNotNil(gotGraph.targets[project.path]?[app.name]) + XCTAssertNotNil(gotGraph.targets[packageProject.path]?[directPackageProduct.name]) + XCTAssertNotNil(gotGraph.targets[packageProject.path]?[transitivePackageProduct.name]) + XCTAssertNil(gotGraph.targets[packageProject.path]?[packageDevProduct.name]) + } +} diff --git a/Tests/TuistDependenciesTests/SwiftPackageManager/Utils/PackageInfoMapperTests.swift b/Tests/TuistDependenciesTests/SwiftPackageManager/Utils/PackageInfoMapperTests.swift index ed8c5a13d58..1b0b44415c3 100644 --- a/Tests/TuistDependenciesTests/SwiftPackageManager/Utils/PackageInfoMapperTests.swift +++ b/Tests/TuistDependenciesTests/SwiftPackageManager/Utils/PackageInfoMapperTests.swift @@ -94,7 +94,12 @@ final class PackageInfoMapperTests: TuistUnitTestCase { .test( name: "Target_1", dependencies: [ - .product(name: "Product2", package: "Package2_different_name", condition: nil), + .product( + name: "Product2", + package: "Package2_different_name", + moduleAliases: nil, + condition: nil + ), ] ), ], @@ -148,7 +153,12 @@ final class PackageInfoMapperTests: TuistUnitTestCase { .test( name: "Target_1", dependencies: [ - .product(name: "com.example.dep-1", package: "com.example.dep-1", condition: nil), + .product( + name: "com.example.dep-1", + package: "com.example.dep-1", + moduleAliases: nil, + condition: nil + ), ] ), ], @@ -255,11 +265,13 @@ final class PackageInfoMapperTests: TuistUnitTestCase { .product( name: "Product_2", package: "Package_2", + moduleAliases: nil, condition: .init(platformNames: ["ios"], config: nil) ), .product( name: "Product_3", package: "Package_2", + moduleAliases: nil, condition: .init(platformNames: ["tvos"], config: nil) ), ] @@ -1258,7 +1270,7 @@ final class PackageInfoMapperTests: TuistUnitTestCase { targets: [ .test( name: "Target1", - dependencies: [.product(name: "Dependency1", package: "Package2", condition: nil)] + dependencies: [.product(name: "Dependency1", package: "Package2", moduleAliases: nil, condition: nil)] ), ], platforms: [], @@ -1273,7 +1285,7 @@ final class PackageInfoMapperTests: TuistUnitTestCase { targets: [ .test( name: "Dependency1", - dependencies: [.product(name: "Dependency2", package: "Package3", condition: nil)] + dependencies: [.product(name: "Dependency2", package: "Package3", moduleAliases: nil, condition: nil)] ), ], platforms: [], @@ -2533,7 +2545,7 @@ final class PackageInfoMapperTests: TuistUnitTestCase { targets: [ .test( name: "Target1", - dependencies: [.product(name: "Product2", package: "Package2", condition: nil)] + dependencies: [.product(name: "Product2", package: "Package2", moduleAliases: nil, condition: nil)] ), .test(name: "Dependency1"), ], @@ -2873,6 +2885,7 @@ final class PackageInfoMapperTests: TuistUnitTestCase { "TempuraTesting", "TSCTestSupport", "ViewInspector", + "XCTVapor", ] let allTargets = ["RxSwift"] + testTargets try allTargets @@ -3034,6 +3047,7 @@ final class PackageInfoMapperTests: TuistUnitTestCase { .product( name: "Product2", package: "Package2", + moduleAliases: nil, condition: .init(platformNames: ["ios"], config: nil) ), ] diff --git a/Tests/TuistDependenciesTests/SwiftPackageManager/Utils/SwiftPackageManagerGraphGeneratorTests.swift b/Tests/TuistDependenciesTests/SwiftPackageManager/Utils/SwiftPackageManagerGraphGeneratorTests.swift index c14f926e9c5..f05c3ac5bd7 100644 --- a/Tests/TuistDependenciesTests/SwiftPackageManager/Utils/SwiftPackageManagerGraphGeneratorTests.swift +++ b/Tests/TuistDependenciesTests/SwiftPackageManager/Utils/SwiftPackageManagerGraphGeneratorTests.swift @@ -501,7 +501,7 @@ extension TuistCore.DependenciesGraph { var mergedExternalDependencies: [String: [ProjectDescription.TargetDependency]] = externalDependencies - other.externalDependencies.forEach { name, dependency in + for (name, dependency) in other.externalDependencies { if let alreadyPresent = mergedExternalDependencies[name] { fatalError("Dupliacted Entry(\(name), \(alreadyPresent), \(dependency)") } diff --git a/Tests/TuistEnvKitTests/Installer/BuildCopierTests.swift b/Tests/TuistEnvKitTests/Installer/BuildCopierTests.swift index 9887ba034c7..6b32342d9f5 100644 --- a/Tests/TuistEnvKitTests/Installer/BuildCopierTests.swift +++ b/Tests/TuistEnvKitTests/Installer/BuildCopierTests.swift @@ -41,7 +41,7 @@ final class BuildCopierTests: XCTestCase { let toPath = toDir.path // Creating files - try BuildCopier.files.forEach { file in + for file in BuildCopier.files { let filePath = fromPath.appending(component: file) try Data().write(to: filePath.url) } diff --git a/Tests/TuistEnvKitTests/Versions/VersionsControllerTests.swift b/Tests/TuistEnvKitTests/Versions/VersionsControllerTests.swift index 76d8d57a39b..6d430756a64 100644 --- a/Tests/TuistEnvKitTests/Versions/VersionsControllerTests.swift +++ b/Tests/TuistEnvKitTests/Versions/VersionsControllerTests.swift @@ -67,8 +67,8 @@ final class VersionsControllerTests: TuistUnitTestCase { "12.2.0", "2.18.0", ] - try versions.forEach { - try fileHandler.createFolder(environment.versionsDirectory.appending(component: $0)) + for version in versions { + try fileHandler.createFolder(environment.versionsDirectory.appending(component: version)) } // When diff --git a/Tests/TuistGenerateAcceptanceTests/GenerateAcceptanceTests.swift b/Tests/TuistGenerateAcceptanceTests/GenerateAcceptanceTests.swift new file mode 100644 index 00000000000..5bcd694ee9f --- /dev/null +++ b/Tests/TuistGenerateAcceptanceTests/GenerateAcceptanceTests.swift @@ -0,0 +1,941 @@ +import TSCBasic +import TuistAcceptanceTesting +import TuistSupport +import TuistSupportTesting +import XcodeProj +import XCTest + +/// Generate a new project using Tuist (suite 1) +final class GenerateAcceptanceTestiOSAppWithTests: TuistAcceptanceTestCase { + func test_ios_app_with_tests() async throws { + try setUpFixture(.iosAppWithTests) + try await run(GenerateCommand.self) + try await run(BuildCommand.self) + } +} + +final class GenerateAcceptanceTestiOSAppWithFrameworks: TuistAcceptanceTestCase { + func test_ios_app_with_frameworks() async throws { + try setUpFixture(.iosAppWithFrameworks) + try await run(GenerateCommand.self) + try await run(BuildCommand.self) + try await XCTAssertProductWithDestinationContainsInfoPlistKey( + "Framework1.framework", + destination: "Debug-iphonesimulator", + key: "Test" + ) + } +} + +final class GenerateAcceptanceTestiOSAppWithHeaders: TuistAcceptanceTestCase { + func test_ios_app_with_headers() async throws { + try setUpFixture(.iosAppWithHeaders) + try await run(GenerateCommand.self) + try await run(BuildCommand.self) + } +} + +final class GenerateAcceptanceTestInvalidWorkspaceManifestName: TuistAcceptanceTestCase { + func test_invalid_workspace_manifest_name() async throws { + try setUpFixture(.invalidWorkspaceManifestName) + do { + try await run(GenerateCommand.self) + XCTFail("Generate command should have failed") + } catch { + XCTAssertEqual(String(describing: error), "Manifest not found at path \(fixturePath.pathString)") + } + } +} + +// TODO: Fix (this test has an issue in GitHub actions due to a missing tvOS platform) +// final class GenerateAcceptanceTestiOSAppWithSDK: TuistAcceptanceTestCase { +// func test_ios_app_with_sdk() async throws { +// try setUpFixture("ios_app_with_sdk") +// try await run(GenerateCommand.self) +// try await run(BuildCommand.self) +// try await run(BuildCommand.self, "MacFramework", "--platform", "macOS") +// try await run(BuildCommand.self, "TVFramework", "--platform", "tvOS") +// } +// } + +final class GenerateAcceptanceTestiOSAppWithFrameworkAndResources: TuistAcceptanceTestCase { + func test_ios_app_with_framework_and_resources() async throws { + try setUpFixture(.iosAppWithFrameworkAndResources) + try await run(GenerateCommand.self) + try await run(BuildCommand.self) + for resource in [ + "tuist.png", + "Examples/item.json", + "Examples/list.json", + "Assets.car", + "resource.txt", + "en.lproj/Greetings.strings", + "fr.lproj/Greetings.strings", + "resource_without_extension", + "StaticFrameworkResources.bundle", + "StaticFramework2Resources.bundle", + "StaticFramework3_StaticFramework3.bundle", + "StaticFramework4_StaticFramework4.bundle", + ] { + try await XCTAssertProductWithDestinationContainsResource( + "App.app", + destination: "Debug-iphonesimulator", + resource: resource + ) + } + try await XCTAssertProductWithDestinationDoesNotContainResource( + "App.app", + destination: "Debug-iphonesimulator", + resource: "do_not_include.dat" + ) + try await XCTAssertProductWithDestinationContainsResource( + "StaticFrameworkResources.bundle", + destination: "Debug-iphonesimulator", + resource: "tuist-bundle.png" + ) + try await XCTAssertProductWithDestinationContainsResource( + "StaticFramework2Resources.bundle", + destination: "Debug-iphonesimulator", + resource: "StaticFramework2Resources-tuist.png" + ) + try await XCTAssertProductWithDestinationContainsResource( + "StaticFramework3_StaticFramework3.bundle", + destination: "Debug-iphonesimulator", + resource: "StaticFramework3Resources-tuist.png" + ) + try await XCTAssertProductWithDestinationContainsResource( + "StaticFramework4_StaticFramework4.bundle", + destination: "Debug-iphonesimulator", + resource: "StaticFramework4Resources-tuist.png" + ) + try XCTAssertDirectoryContentEqual( + fixturePath.appending(components: "App", "Derived", "Sources"), + [ + "TuistBundle+App.swift", + "TuistStrings+App.swift", + "TuistAssets+App.swift", + "TuistFonts+App.swift", + "TuistPlists+App.swift", + ] + ) + try XCTAssertDirectoryContentEqual( + fixturePath.appending(components: "StaticFramework3", "Derived", "Sources"), + [ + "TuistAssets+StaticFramework3.swift", + "TuistBundle+StaticFramework3.swift", + ] + ) + try XCTAssertProductWithDestinationDoesNotContainHeaders( + "App.app", + destination: "Debug-iphonesimulator" + ) + } +} + +final class GenerateAcceptanceTestIosAppWithCustomDevelopmentRegion: TuistAcceptanceTestCase { + func test_ios_app_with_custom_development_region() async throws { + try setUpFixture(.iosAppWithCustomDevelopmentRegion) + try await run(GenerateCommand.self) + try await run(BuildCommand.self) + for resource in [ + "en.lproj/Greetings.strings", + "fr.lproj/Greetings.strings", + "fr-CA.lproj/Greetings.strings", + ] { + try await XCTAssertProductWithDestinationContainsResource( + "App.app", + destination: "Debug-iphonesimulator", + resource: resource + ) + } + + XCTAssertTrue( + try FileHandler.shared.readTextFile( + fixturePath.appending(components: "Derived", "Sources", "TuistStrings+App.swift") + ) + .contains( + """ + public static let evening = AppStrings.tr("Greetings", "evening") + """ + ) + ) + } +} + +final class GenerateAcceptanceTestiOSAppWithCustomResourceParserOptions: TuistAcceptanceTestCase { + func test_ios_app_with_custom_resource_parser_options() async throws { + try setUpFixture(.iosWppWithCustomResourceParserOptions) + try await run(GenerateCommand.self) + try await run(BuildCommand.self) + for resource in [ + "en.lproj/Greetings.strings", + "fr.lproj/Greetings.strings", + ] { + try await XCTAssertProductWithDestinationContainsResource( + "App.app", + destination: "Debug-iphonesimulator", + resource: resource + ) + } + + XCTAssertTrue( + try FileHandler.shared.readTextFile( + fixturePath.appending(components: "Derived", "Sources", "TuistStrings+App.swift") + ) + .contains( + """ + public static let evening = AppStrings.tr("Greetings", "Good/evening") + """ + ) + ) + XCTAssertTrue( + try FileHandler.shared.readTextFile( + fixturePath.appending(components: "Derived", "Sources", "TuistStrings+App.swift") + ) + .contains( + """ + public static let morning = AppStrings.tr("Greetings", "Good/morning") + """ + ) + ) + } +} + +final class GenerateAcceptanceTestiOSAppWithFrameworkLinkingStaticFramework: TuistAcceptanceTestCase { + func test_ios_app_with_framework_linking_static_framework() async throws { + try setUpFixture(.iosAppWithFrameworkLinkingStaticFramework) + try await run(BuildCommand.self) + + try await XCTAssertProductWithDestinationContainsResource( + "App.app", + destination: "Debug-iphonesimulator", + resource: "Frameworks/Framework1.framework/Framework1" + ) + for resource in [ + "Frameworks/Framework2.framework/Framework2", + "Frameworks/Framework3.framework/Framework3", + "Frameworks/Framework4.framework/Framework4", + ] { + try await XCTAssertProductWithDestinationDoesNotContainResource( + "App.app", + destination: "Debug-iphonesimulator", + resource: resource + ) + } + try XCTAssertProductWithDestinationDoesNotContainHeaders("App.app", destination: "Debug-iphonesimulator") + } +} + +final class GenerateAcceptanceTestsiOSAppWithCustomScheme: TuistAcceptanceTestCase { + func test_ios_app_with_custom_scheme() async throws { + try setUpFixture(.iosAppWithCustomScheme) + try await run(BuildCommand.self) + try await run(BuildCommand.self, "App-Debug") + try await run(BuildCommand.self, "App-Release") + try await run(BuildCommand.self, "App-Local") + } +} + +final class GenerateAcceptanceTestiOSAppWithLocalSwiftPackage: TuistAcceptanceTestCase { + func test_ios_app_with_local_swift_package() async throws { + try setUpFixture(.iosAppWithLocalSwiftPackage) + try await run(BuildCommand.self) + } +} + +final class GenerateAcceptanceTestiOSAppWithMultiConfigs: TuistAcceptanceTestCase { + func test_ios_app_with_multi_configs() async throws { + try setUpFixture(.iosAppWithMultiConfigs) + try await run(GenerateCommand.self) + try await XCTAssertSchemeContainsBuildSettings( + "App", + configuration: "Debug", + buildSettingKey: "CUSTOM_FLAG", + buildSettingValue: "Debug" + ) + try await XCTAssertSchemeContainsBuildSettings( + "App", + configuration: "Beta", + buildSettingKey: "CUSTOM_FLAG", + buildSettingValue: "Beta" + ) + try await XCTAssertSchemeContainsBuildSettings( + "App", + configuration: "Release", + buildSettingKey: "CUSTOM_FLAG", + buildSettingValue: "Release" + ) + try await XCTAssertSchemeContainsBuildSettings( + "Framework2", + configuration: "Debug", + buildSettingKey: "CUSTOM_FLAG", + buildSettingValue: "Debug" + ) + try await XCTAssertSchemeContainsBuildSettings( + "Framework2", + configuration: "Beta", + buildSettingKey: "CUSTOM_FLAG", + buildSettingValue: "Target.Beta" + ) + try await XCTAssertSchemeContainsBuildSettings( + "Framework2", + configuration: "Release", + buildSettingKey: "CUSTOM_FLAG", + buildSettingValue: "Release" + ) + } +} + +final class GenerateAcceptanceTestiOSAppWithIncompatibleXcode: TuistAcceptanceTestCase { + func test_ios_app_with_incompatible_xcode() async throws { + try setUpFixture(.iosAppWithIncompatibleXcode) + do { + try await run(GenerateCommand.self) + XCTFail("Generate should have failed") + } catch { + XCTAssertStandardError( + pattern: "which is not compatible with this project's Xcode version requirement of 3.2.1." + ) + XCTAssertEqual( + (error as? FatalError)?.description, + "Fatal linting issues found" + ) + } + } +} + +// TODO: Find a different build tool plugin. SwiftLintPlugin imports swift-syntax that takes a _very_ long time to build +// final class GenerateAcceptanceTestiOSAppWithActions: TuistAcceptanceTestCase { +// func test_ios_app_with_actions() async throws { +// try setUpFixture("ios_app_with_actions") +// try await run(GenerateCommand.self) +// try await run(BuildCommand.self) +// +// let xcodeproj = try XcodeProj( +// pathString: fixturePath.appending(components: "App", "App.xcodeproj").pathString +// ) +// let target = try XCTUnwrapTarget("App", in: xcodeproj) +// let buildPhases = target.buildPhases +// +// XCTAssertEqual( +// buildPhases.first?.name(), +// "Tuist" +// ) +// XCTAssertEqual( +// buildPhases.last?.name(), +// "Rocks" +// ) +// let phaseWithDependency = try XCTUnwrap( +// buildPhases +// .first(where: { $0.name() == "PhaseWithDependency" }) +// as? PBXShellScriptBuildPhase +// ) +// XCTAssertEqual(phaseWithDependency.dependencyFile, "$TEMP_DIR/dependencies.d") +// +// let appWithSpaceXcodeproj = try XcodeProj( +// pathString: fixturePath.appending(components: "App With Space", "AppWithSpace.xcodeproj").pathString +// ) +// let appWithSpaceTarget = try XCTUnwrapTarget("AppWithSpace", in: appWithSpaceXcodeproj) +// XCTAssertEqual( +// appWithSpaceTarget.buildPhases.first?.name(), +// "Run script" +// ) +// } +// } + +final class GenerateAcceptanceTestiOSAppWithBuildVariables: TuistAcceptanceTestCase { + func test_ios_app_with_build_variables() async throws { + try setUpFixture(.iosAppWithBuildVariables) + try await run(GenerateCommand.self) + let xcodeproj = try XcodeProj( + pathString: fixturePath.appending(components: "App", "App.xcodeproj").pathString + ) + let target = try XCTUnwrapTarget("App", in: xcodeproj) + let buildPhases = target.buildPhases + + XCTAssertEqual( + buildPhases.first?.name(), + "Tuist" + ) + XCTAssertEqual( + (buildPhases.first as? PBXShellScriptBuildPhase)?.outputPaths, + ["$(DERIVED_FILE_DIR)/output.txt"] + ) + try await run(BuildCommand.self) + } +} + +final class GenerateAcceptanceTestiOSAppWithRemoteSwiftPackage: TuistAcceptanceTestCase { + func test_ios_app_with_remote_swift_package() async throws { + try setUpFixture(.iosAppWithRemoteSwiftPackage) + try await run(GenerateCommand.self) + try await run(BuildCommand.self) + } +} + +final class GenerateAcceptanceTestVisionOSAppWithRemoteSwiftPackage: TuistAcceptanceTestCase { + func test_visionos_app() async throws { + try setUpFixture(.visionosApp) + try await run(GenerateCommand.self) +// TODO: Fix +// try await run(BuildCommand.self) + } +} + +final class GenerateAcceptanceTestiOSAppWithLocalBinarySwiftPackage: TuistAcceptanceTestCase { + func test_ios_app_with_local_binary_swift_package() async throws { + try setUpFixture(.iosAppWithLocalBinarySwiftPackage) + try await run(GenerateCommand.self) + try await run(BuildCommand.self) + } +} + +final class GenerateAcceptanceTestiOSAppWithExtensions: TuistAcceptanceTestCase { + func test_ios_app_with_extensions() async throws { + try setUpFixture(.iosAppWithExtensions) + try await run(GenerateCommand.self) + try await run(BuildCommand.self, "App") + + try await XCTAssertProductWithDestinationContainsExtension( + "App.app", + destination: "Debug-iphonesimulator", + extension: "StickersPackExtension" + ) + try await XCTAssertProductWithDestinationContainsExtension( + "App.app", + destination: "Debug-iphonesimulator", + extension: "NotificationServiceExtension" + ) + try await XCTAssertProductWithDestinationContainsExtensionKitExtension( + "App.app", + destination: "Debug-iphonesimulator", + extension: "AppIntentExtension" + ) + try XCTAssertProductWithDestinationDoesNotContainHeaders( + "App.app", + destination: "Debug-iphonesimulator" + ) + } +} + +// TODO: Fix – tvOS +// final class GenerateAcceptanceTestTvOSAppWithExtensions: TuistAcceptanceTestCase { +// func test_tvos_app_with_extensions() async throws { +// try setUpFixture("tvos_app_with_extensions") +// try await run(GenerateCommand.self) +// try await run(BuildCommand.self) +// try await XCTAssertProductWithDestinationContainsExtension( +// "App.app", +// destination: "Debug-appletvsimulator", +// extension: "TopShelfExtension" +// ) +// try XCTAssertProductWithDestinationDoesNotContainHeaders( +// "App.app", +// destination: "Debug-appletvsimulator" +// ) +// } +// } + +final class GenerateAcceptanceTestiOSAppWithWatchApp2: TuistAcceptanceTestCase { + func test_ios_app_with_watchapp2() async throws { + try setUpFixture(.iosAppWithWatchapp2) + try await run(GenerateCommand.self) + try await run(BuildCommand.self, "App") + try await XCTAssertProductWithDestinationContainsResource( + "App.app", + destination: "Debug-iphonesimulator", + resource: "Watch/WatchApp.app" + ) + try await XCTAssertProductWithDestinationContainsExtension( + "WatchApp.app", + destination: "Debug-watchsimulator", + extension: "WatchAppExtension" + ) + try XCTAssertProductWithDestinationDoesNotContainHeaders( + "App.app", + destination: "Debug-iphonesimulator" + ) + try XCTAssertProductWithDestinationDoesNotContainHeaders( + "WatchApp.app", + destination: "Debug-watchsimulator" + ) + } +} + +final class GenerateAcceptanceTestInvalidManifest: TuistAcceptanceTestCase { + func test_invalid_manifest() async throws { + try setUpFixture(.invalidManifest) + do { + try await run(GenerateCommand.self) + XCTFail("Generate command should have failed") + } catch let error as FatalError { + XCTAssertTrue(error.description.contains("error: expected ',' separator")) + } + } +} + +final class GenerateAcceptanceTestiOSAppLarge: TuistAcceptanceTestCase { + func test_ios_app_large() async throws { + try setUpFixture(.iosAppLarge) + try await run(GenerateCommand.self) + } +} + +final class GenerateAcceptanceTestiOSWorkspaceWithDependencyCycle: TuistAcceptanceTestCase { + func test_ios_workspace_with_dependency_cycle() async throws { + try setUpFixture(.iosWorkspaceWithDependencyCycle) + do { + try await run(GenerateCommand.self) + XCTFail("Generate command should have failed") + } catch let error as FatalError { + XCTAssertTrue(error.description.contains("Found circular dependency between targets")) + } + } +} + +final class GenerateAcceptanceTestFrameworkWithEnvironmentVariables: TuistAcceptanceTestCase { + func test_framework_with_environment_variables() async throws { + try setUpFixture(.frameworkWithEnvironmentVariables) + environment.manifestLoadingVariables["TUIST_FRAMEWORK_NAME"] = "FrameworkA" + try await run(GenerateCommand.self) + try await run(BuildCommand.self, "FrameworkA") + environment.manifestLoadingVariables["TUIST_FRAMEWORK_NAME"] = "FrameworkB" + try await run(GenerateCommand.self) + try await run(BuildCommand.self, "FrameworkB") + } +} + +final class GenerateAcceptanceTestiOSAppWithCoreData: TuistAcceptanceTestCase { + func test_ios_app_with_coredata() async throws { + try setUpFixture(.iosAppWithCoreData) + try await run(GenerateCommand.self) + try await run(BuildCommand.self) + for resource in [ + "Users.momd", + "Unversioned.momd", + "UsersAutoDetect.momd", + "1_2.cdm", + ] { + try await XCTAssertProductWithDestinationContainsResource( + "App.app", + destination: "Debug-iphonesimulator", + resource: resource + ) + } + } +} + +final class GenerateAcceptanceTestiOSAppWithAppClip: TuistAcceptanceTestCase { + func test_ios_app_with_appclip() async throws { + try setUpFixture(.iosAppWithAppClip) + try await run(GenerateCommand.self) + try await run(BuildCommand.self) + try await XCTAssertProductWithDestinationContainsAppClipWithArchitecture( + "App.app", + destination: "Debug-iphonesimulator", + appClip: "AppClip1", + architecture: "x86_64" + ) + try XCTAssertFrameworkEmbedded("Framework", by: "AppClip1") + } +} + +final class GenerateAcceptanceTestCommandLineToolBase: TuistAcceptanceTestCase { + func test_command_line_tool_basic() async throws { + try setUpFixture(.commandLineToolBasic) + try await run(GenerateCommand.self) + try await run(BuildCommand.self, "CommandLineTool") + } +} + +final class GenerateAcceptanceTestCommandLineToolWithStaticLibrary: TuistAcceptanceTestCase { + func test_command_line_tool_with_static_library() async throws { + try setUpFixture(.commandLineToolWithStaticLibrary) + try await run(GenerateCommand.self) + try await run(BuildCommand.self, "CommandLineTool") + } +} + +final class GenerateAcceptanceTestCommandLineToolWithDynamicLibrary: TuistAcceptanceTestCase { + func test_command_line_tool_with_dynamic_library() async throws { + try setUpFixture(.commandLineToolWithDynamicLibrary) + try await run(GenerateCommand.self) + try await run(BuildCommand.self, "CommandLineTool") + } +} + +final class GenerateAcceptanceTestCommandLineToolWithDynamicFramework: TuistAcceptanceTestCase { + func test_command_line_tool_with_dynamic_framework() async throws { + try setUpFixture(.commandLineToolWithDynamicFramework) + try await run(GenerateCommand.self) + try await run(BuildCommand.self, "CommandLineTool") + } +} + +final class GenerateAcceptanceTestmacOSAppWithCopyFiles: TuistAcceptanceTestCase { + func test_macos_app_with_copy_files() async throws { + try setUpFixture(.macosAppWithCopyFiles) + try await run(GenerateCommand.self) + try await run(BuildCommand.self) + + let xcodeproj = try XcodeProj( + pathString: xcodeprojPath.pathString + ) + let target = try XCTUnwrapTarget("App", in: xcodeproj) + let buildPhases = target.buildPhases + + XCTAssertTrue( + buildPhases.contains(where: { $0.name() == "Copy Templates" }) + ) + } +} + +final class GenerateAcceptanceTestManifestWithLogs: TuistAcceptanceTestCase { + func test_manifest_with_logs() async throws { + try setUpFixture(.manifestWithLogs) + try await run(GenerateCommand.self) + XCTAssertStandardOutput(pattern: "Target name - App") + } +} + +final class GenerateAcceptanceTestProjectWithFileHeaderTemplate: TuistAcceptanceTestCase { + func test_project_with_file_header_template() async throws { + try setUpFixture(.projectWithFileHeaderTemplate) + try await run(GenerateCommand.self) + XCTAssertTrue( + FileHandler.shared.exists( + xcodeprojPath.appending( + components: [ + "xcshareddata", + "IDETemplateMacros.plist", + ] + ) + ) + ) + } +} + +final class GenerateAcceptanceTestProjectWithInlineFileHeaderTemplate: TuistAcceptanceTestCase { + func test_project_with_inline_file_header_template() async throws { + try setUpFixture(.projectWithInlineFileHeaderTemplate) + try await run(GenerateCommand.self) + XCTAssertTrue( + FileHandler.shared.exists( + xcodeprojPath.appending( + components: [ + "xcshareddata", + "IDETemplateMacros.plist", + ] + ) + ) + ) + } +} + +final class GenerateAcceptanceTestWorkspaceWithFileHeaderTemplate: TuistAcceptanceTestCase { + func test_workspace_with_file_header_template() async throws { + try setUpFixture(.workspaceWithFileHeaderTemplate) + try await run(GenerateCommand.self) + XCTAssertTrue( + FileHandler.shared.exists( + workspacePath.appending( + components: [ + "xcshareddata", + "IDETemplateMacros.plist", + ] + ) + ) + ) + } +} + +final class GenerateAcceptanceTestWorkspaceWithInlineFileHeaderTemplate: TuistAcceptanceTestCase { + func test_workspace_with_inline_file_header_template() async throws { + try setUpFixture(.workspaceWithInlineFileHeaderTemplate) + try await run(GenerateCommand.self) + XCTAssertTrue( + FileHandler.shared.exists( + workspacePath.appending( + components: [ + "xcshareddata", + "IDETemplateMacros.plist", + ] + ) + ) + ) + } +} + +final class GenerateAcceptanceTestiOSAppWithFrameworkAndDisabledResources: TuistAcceptanceTestCase { + func test_ios_app_with_framework_and_disabled_resources() async throws { + try setUpFixture(.iosAppWithFrameworkAndDisabledResources) + try await run(GenerateCommand.self) + XCTAssertFalse( + FileHandler.shared.exists( + fixturePath.appending( + components: [ + "App", + "Derived", + "Sources", + "TuistBundle+App.swift", + ] + ) + ) + ) + XCTAssertFalse( + FileHandler.shared.exists( + fixturePath.appending( + components: [ + "Framework1", + "Derived", + "Sources", + "TuistBundle+Framework1.swift", + ] + ) + ) + ) + XCTAssertFalse( + FileHandler.shared.exists( + fixturePath.appending( + components: [ + "StaticFramework", + "Derived", + "Sources", + "TuistBundle+StaticFramework.swift", + ] + ) + ) + ) + } +} + +final class GenerateAcceptanceTestmacOSAppWithExtensions: TuistAcceptanceTestCase { + func test_macos_app_with_extensions() async throws { + try setUpFixture(.macosAppWithExtensions) + let sdkPkgPath = sourceRootPath + .appending( + components: [ + "fixtures", + "resources", + "WorkflowExtensionsSDK.pkg", + ] + ) + if try !FileHandler.shared.exists( + AbsolutePath(validating: "/Library/Developer/SDKs/WorkflowExtensionSDK.sdk") + ) { + try System.shared.run(["sudo", "installer", "-package", sdkPkgPath.pathString, "-target", "/"]) + } + + try await run(GenerateCommand.self) + try await run(BuildCommand.self) + } +} + +final class GenerateAcceptanceTestiOSAppWithImplicitDependencies: TuistAcceptanceTestCase { + func test_ios_app_with_implicit_dependencies() async throws { + try setUpFixture(.iosAppWithImplicitDependencies) + try await run(BuildCommand.self, "FrameworkC") + do { + try await run(BuildCommand.self, "App") + XCTFail("Building app should fail as FrameworkA has an implicit dependency on FrameworkB") + } catch let error as FatalError { + XCTAssertTrue( + error.description.contains("The 'xcodebuild' command exited with error code 65 and message") + ) + } + } +} + +extension TuistAcceptanceTestCase { + private func resourcePath( + for productName: String, + destination: String, + resource: String + ) throws -> AbsolutePath { + let productPath = try productPath(for: productName, destination: destination) + if let resource = FileHandler.shared.glob(productPath, glob: "**/\(resource)").first { + return resource + } else { + XCTFail("Could not find resource \(resource) for product \(productName) and destination \(destination)") + throw XCTUnwrapError.nilValueDetected + } + } + + func XCTAssertSchemeContainsBuildSettings( + _ scheme: String, + configuration: String, + buildSettingKey: String, + buildSettingValue: String, + file: StaticString = #file, + line: UInt = #line + ) async throws { + let buildSettings = try await System.shared.runAndCollectOutput( + [ + "/usr/bin/xcodebuild", + "-scheme", + scheme, + "-workspace", + workspacePath.pathString, + "-configuration", + configuration, + "-showBuildSettings", + ] + ) + + guard buildSettings.standardOutput.contains("\(buildSettingKey) = \"\(buildSettingValue)\"") + else { + XCTFail( + "Couldn't find \(buildSettingKey) = \(buildSettingValue) for scheme \(scheme) and configuration \(configuration)", + file: file, + line: line + ) + return + } + } + + func XCTAssertProductWithDestinationContainsAppClipWithArchitecture( + _ product: String, + destination: String, + appClip: String, + architecture: String, + file: StaticString = #file, + line: UInt = #line + ) async throws { + let productPath = try productPath( + for: product, + destination: destination + ) + + guard let appClipPath = FileHandler.shared.glob(productPath, glob: "AppClips/\(appClip).app").first, + FileHandler.shared.exists(appClipPath) + else { + XCTFail( + "App clip \(appClip) not found for product \(product) and destination \(destination)", + file: file, + line: line + ) + return + } + + let fileInfo = try await System.shared.runAndCollectOutput( + [ + "file", + appClipPath.appending(component: appClip).pathString, + ] + ) + XCTAssertTrue(fileInfo.standardOutput.contains(architecture)) + } + + func XCTAssertProductWithDestinationContainsExtension( + _ product: String, + destination: String, + extension: String, + file: StaticString = #file, + line: UInt = #line + ) async throws { + let productPath = try productPath( + for: product, + destination: destination + ) + + guard let extensionPath = FileHandler.shared.glob(productPath, glob: "Plugins/\(`extension`).appex").first, + FileHandler.shared.exists(extensionPath) + else { + XCTFail( + "Extension \(`extension`) not found for product \(product) and destination \(destination)", + file: file, + line: line + ) + return + } + } + + func XCTAssertProductWithDestinationContainsExtensionKitExtension( + _ product: String, + destination: String, + extension: String, + file: StaticString = #file, + line: UInt = #line + ) async throws { + let productPath = try productPath( + for: product, + destination: destination + ) + + guard let extensionPath = FileHandler.shared.glob(productPath, glob: "Extensions/\(`extension`).appex").first, + FileHandler.shared.exists(extensionPath) + else { + XCTFail( + "ExtensionKit \(`extension`) not found for product \(product) and destination \(destination)", + file: file, + line: line + ) + return + } + } + + fileprivate func XCTAssertProductWithDestinationContainsResource( + _ product: String, + destination: String, + resource: String, + file: StaticString = #file, + line: UInt = #line + ) async throws { + let resourcePath = try resourcePath( + for: product, + destination: destination, + resource: resource + ) + + if !FileHandler.shared.exists(resourcePath) { + XCTFail( + "Resource \(resource) not found for product \(product) and destination \(destination)", + file: file, + line: line + ) + } + } + + fileprivate func XCTAssertProductWithDestinationDoesNotContainResource( + _ product: String, + destination: String, + resource: String, + file: StaticString = #file, + line: UInt = #line + ) async throws { + let productPath = try productPath(for: product, destination: destination) + if !FileHandler.shared.glob(productPath, glob: "**/\(resource)").isEmpty { + XCTFail("Resource \(resource) found for product \(product) and destination \(destination)", file: file, line: line) + } + } + + fileprivate func XCTAssertProductWithDestinationContainsInfoPlistKey( + _ product: String, + destination: String, + key: String, + file: StaticString = #file, + line: UInt = #line + ) async throws { + let infoPlistPath = try resourcePath( + for: product, + destination: destination, + resource: "Info.plist" + ) + let output = try await System.shared.runAndCollectOutput( + [ + "/usr/libexec/PlistBuddy", + "-c", + "print :\(key)", + infoPlistPath.pathString, + ] + ) + + if output.standardOutput.isEmpty { + XCTFail( + "Key \(key) not found in the \(product) Info.plist", + file: file, + line: line + ) + } + } +} diff --git a/Tests/TuistGenerateOneAcceptanceTests/GenerateOneAcceptanceTests.swift b/Tests/TuistGenerateOneAcceptanceTests/GenerateOneAcceptanceTests.swift deleted file mode 100644 index defc2fd1837..00000000000 --- a/Tests/TuistGenerateOneAcceptanceTests/GenerateOneAcceptanceTests.swift +++ /dev/null @@ -1,308 +0,0 @@ -import TSCBasic -import TuistAcceptanceTesting -import TuistSupport -import TuistSupportTesting -import XCTest - -/// Generate a new project using Tuist (suite 1) -final class GenerateOneAcceptanceTestiOSAppWithTests: TuistAcceptanceTestCase { - func test_ios_app_with_tests() async throws { - try setUpFixture("ios_app_with_tests") - try await run(GenerateCommand.self) - try await run(BuildCommand.self) - } -} - -final class GenerateOneAcceptanceTestiOSAppWithFrameworks: TuistAcceptanceTestCase { - func test_ios_app_with_frameworks() async throws { - try setUpFixture("ios_app_with_frameworks") - try await run(GenerateCommand.self) - try await run(BuildCommand.self) - try await XCTAssertProductWithDestinationContainsInfoPlistKey( - "Framework1.framework", - destination: "Debug-iphonesimulator", - key: "Test" - ) - } -} - -final class GenerateOneAcceptanceTestiOSAppWithHeaders: TuistAcceptanceTestCase { - func test_ios_app_with_headers() async throws { - try setUpFixture("ios_app_with_headers") - try await run(GenerateCommand.self) - try await run(BuildCommand.self) - } -} - -final class GenerateOneAcceptanceTestInvalidWorkspaceManifestName: TuistAcceptanceTestCase { - func test_invalid_workspace_manifest_name() async throws { - try setUpFixture("invalid_workspace_manifest_name") - do { - try await run(GenerateCommand.self) - XCTFail("Generate command should have failed") - } catch { - XCTAssertEqual(String(describing: error), "Manifest not found at path \(fixturePath.pathString)") - } - } -} - -final class GenerateOneAcceptanceTestiOSAppWithSDK: TuistAcceptanceTestCase { - func test_ios_app_with_sdk() async throws { - try setUpFixture("ios_app_with_sdk") - try await run(GenerateCommand.self) - try await run(BuildCommand.self) - try await run(BuildCommand.self, "MacFramework", "--platform", "macOS") - try await run(BuildCommand.self, "TVFramework", "--platform", "tvOS") - } -} - -final class GenerateOneAcceptanceTestiOSAppWithFrameworkAndResources: TuistAcceptanceTestCase { - func test_ios_app_with_framework_and_resources() async throws { - try setUpFixture("ios_app_with_framework_and_resources") - try await run(GenerateCommand.self) - try await run(BuildCommand.self) - for resource in [ - "tuist.png", - "Examples/item.json", - "Examples/list.json", - "Assets.car", - "resource.txt", - "en.lproj/Greetings.strings", - "fr.lproj/Greetings.strings", - "resource_without_extension", - "StaticFrameworkResources.bundle", - "StaticFramework2Resources.bundle", - "StaticFramework3_StaticFramework3.bundle", - "StaticFramework4_StaticFramework4.bundle", - ] { - try await XCTAssertProductWithDestinationContainsResource( - "App.app", - destination: "Debug-iphonesimulator", - resource: resource - ) - } - try await XCTAssertProductWithDestinationDoesNotContainResource( - "App.app", - destination: "Debug-iphonesimulator", - resource: "do_not_include.dat" - ) - try await XCTAssertProductWithDestinationContainsResource( - "StaticFrameworkResources.bundle", - destination: "Debug-iphonesimulator", - resource: "tuist-bundle.png" - ) - try await XCTAssertProductWithDestinationContainsResource( - "StaticFramework2Resources.bundle", - destination: "Debug-iphonesimulator", - resource: "StaticFramework2Resources-tuist.png" - ) - try await XCTAssertProductWithDestinationContainsResource( - "StaticFramework3_StaticFramework3.bundle", - destination: "Debug-iphonesimulator", - resource: "StaticFramework3Resources-tuist.png" - ) - try await XCTAssertProductWithDestinationContainsResource( - "StaticFramework4_StaticFramework4.bundle", - destination: "Debug-iphonesimulator", - resource: "StaticFramework4Resources-tuist.png" - ) - try XCTAssertDirectoryContentEqual( - fixturePath.appending(components: "App", "Derived", "Sources"), - [ - "TuistBundle+App.swift", - "TuistStrings+App.swift", - "TuistAssets+App.swift", - "TuistFonts+App.swift", - "TuistPlists+App.swift", - ] - ) - try XCTAssertDirectoryContentEqual( - fixturePath.appending(components: "StaticFramework3", "Derived", "Sources"), - [ - "TuistAssets+StaticFramework3.swift", - "TuistBundle+StaticFramework3.swift", - ] - ) - try XCTAssertProductWithDestinationDoesNotContainHeaders( - "App.app", - destination: "Debug-iphonesimulator" - ) - } -} - -final class GenerateOneAcceptanceTestIosAppWithCustomDevelopmentRegion: TuistAcceptanceTestCase { - func test_ios_app_with_custom_development_region() async throws { - try setUpFixture("ios_app_with_custom_development_region") - try await run(GenerateCommand.self) - try await run(BuildCommand.self) - for resource in [ - "en.lproj/Greetings.strings", - "fr.lproj/Greetings.strings", - ] { - try await XCTAssertProductWithDestinationContainsResource( - "App.app", - destination: "Debug-iphonesimulator", - resource: resource - ) - } - - XCTAssertTrue( - try FileHandler.shared.readTextFile( - fixturePath.appending(components: "Derived", "Sources", "TuistStrings+App.swift") - ) - .contains( - """ - public static let evening = AppStrings.tr("Greetings", "evening") - """ - ) - ) - } -} - -final class GenerateOneAcceptanceTestiOSAppWithCustomResourceParserOptions: TuistAcceptanceTestCase { - func test_ios_app_with_custom_resource_parser_options() async throws { - try setUpFixture("ios_app_with_custom_resource_parser_options") - try await run(GenerateCommand.self) - try await run(BuildCommand.self) - for resource in [ - "en.lproj/Greetings.strings", - "fr.lproj/Greetings.strings", - ] { - try await XCTAssertProductWithDestinationContainsResource( - "App.app", - destination: "Debug-iphonesimulator", - resource: resource - ) - } - - XCTAssertTrue( - try FileHandler.shared.readTextFile( - fixturePath.appending(components: "Derived", "Sources", "TuistStrings+App.swift") - ) - .contains( - """ - public static let evening = AppStrings.tr("Greetings", "Good/evening") - """ - ) - ) - XCTAssertTrue( - try FileHandler.shared.readTextFile( - fixturePath.appending(components: "Derived", "Sources", "TuistStrings+App.swift") - ) - .contains( - """ - public static let morning = AppStrings.tr("Greetings", "Good/morning") - """ - ) - ) - } -} - -extension TuistAcceptanceTestCase { - private func headers( - for productName: String, - destination: String - ) throws -> [AbsolutePath] { - let productPath = try productPath(for: productName, destination: destination) - return FileHandler.shared.glob(productPath, glob: "**/*.h") - } - - private func productPath( - for name: String, - destination: String - ) throws -> AbsolutePath { - try XCTUnwrap( - FileHandler.shared.glob(derivedDataPath, glob: "**/Build/**/Products/\(destination)/\(name)/").first - ) - } - - private func resourcePath( - for productName: String, - destination: String, - resource: String - ) throws -> AbsolutePath { - let productPath = try productPath(for: productName, destination: destination) - if let resource = FileHandler.shared.glob(productPath, glob: "**/\(resource)").first { - return resource - } else { - XCTFail("Could not find resource \(resource) for product \(productName) and destination \(destination)") - throw XCTUnwrapError.nilValueDetected - } - } - - func XCTAssertProductWithDestinationDoesNotContainHeaders( - _ product: String, - destination: String, - file: StaticString = #file, - line: UInt = #line - ) throws { - if try !headers(for: product, destination: destination).isEmpty { - XCTFail("Product with name \(product) and destination \(destination) contains headers", file: file, line: line) - } - } - - fileprivate func XCTAssertProductWithDestinationContainsResource( - _ product: String, - destination: String, - resource: String, - file: StaticString = #file, - line: UInt = #line - ) async throws { - let resourcePath = try resourcePath( - for: product, - destination: destination, - resource: resource - ) - - if !FileHandler.shared.exists(resourcePath) { - XCTFail( - "Resource \(resource) not found for product \(product) and destination \(destination)", - file: file, - line: line - ) - } - } - - fileprivate func XCTAssertProductWithDestinationDoesNotContainResource( - _ product: String, - destination: String, - resource: String, - file: StaticString = #file, - line: UInt = #line - ) async throws { - let productPath = try productPath(for: product, destination: destination) - if !FileHandler.shared.glob(productPath, glob: "**/\(resource)").isEmpty { - XCTFail("Resource \(resource) found for product \(product) and destination \(destination)", file: file, line: line) - } - } - - fileprivate func XCTAssertProductWithDestinationContainsInfoPlistKey( - _ product: String, - destination: String, - key: String, - file: StaticString = #file, - line: UInt = #line - ) async throws { - let infoPlistPath = try resourcePath( - for: product, - destination: destination, - resource: "Info.plist" - ) - let output = try await System.shared.runAndCollectOutput( - [ - "/usr/libexec/PlistBuddy", - "-c", - "print :\(key)", - infoPlistPath.pathString, - ] - ) - - if output.standardOutput.isEmpty { - XCTFail( - "Key \(key) not found in the \(product) Info.plist", - file: file, - line: line - ) - } - } -} diff --git a/Tests/TuistGeneratorIntegrationTests/Generator/WorkspaceGeneratorIntegrationTests.swift b/Tests/TuistGeneratorIntegrationTests/Generator/WorkspaceGeneratorIntegrationTests.swift index 556f761044f..b51a11ef1b5 100644 --- a/Tests/TuistGeneratorIntegrationTests/Generator/WorkspaceGeneratorIntegrationTests.swift +++ b/Tests/TuistGeneratorIntegrationTests/Generator/WorkspaceGeneratorIntegrationTests.swift @@ -48,7 +48,7 @@ final class WorkspaceGeneratorIntegrationTests: TuistTestCase { let graphTraverser = GraphTraverser(graph: graph) // When / Then - try (0 ..< 50).forEach { _ in + for _ in 0 ..< 50 { _ = try subject.generate(graphTraverser: graphTraverser) } } diff --git a/Tests/TuistGeneratorIntegrationTests/Generator/XcodeProjWriterTests.swift b/Tests/TuistGeneratorIntegrationTests/Generator/XcodeProjWriterTests.swift index 42592657957..f135e1fa7be 100644 --- a/Tests/TuistGeneratorIntegrationTests/Generator/XcodeProjWriterTests.swift +++ b/Tests/TuistGeneratorIntegrationTests/Generator/XcodeProjWriterTests.swift @@ -97,7 +97,7 @@ final class XcodeProjWriterTests: TuistTestCase { ) // When - try (0 ..< 2).forEach { _ in + for _ in 0 ..< 2 { try subject.write(project: descriptor) } @@ -119,7 +119,7 @@ final class XcodeProjWriterTests: TuistTestCase { ] // When - try schemesWriteOperations.forEach { schemes in + for schemes in schemesWriteOperations { let descriptor = ProjectDescriptor.test( path: path, xcodeprojPath: xcodeProjPath, @@ -150,7 +150,7 @@ final class XcodeProjWriterTests: TuistTestCase { ] // When - try schemesWriteOperations.forEach { schemes in + for schemes in schemesWriteOperations { let descriptor = ProjectDescriptor.test( path: path, xcodeprojPath: xcodeProjPath, @@ -182,7 +182,7 @@ final class XcodeProjWriterTests: TuistTestCase { ] // When - try schemesWriteOperations.forEach { schemes in + for schemes in schemesWriteOperations { let descriptor = WorkspaceDescriptor.test( path: path, xcworkspacePath: xcworkspacePath, @@ -213,7 +213,7 @@ final class XcodeProjWriterTests: TuistTestCase { ] // When - try schemesWriteOperations.forEach { schemes in + for schemes in schemesWriteOperations { let descriptor = WorkspaceDescriptor.test( path: path, xcworkspacePath: xcworkspacePath, diff --git a/Tests/TuistGeneratorTests/Generator/BuildPhaseGeneratorTests.swift b/Tests/TuistGeneratorTests/Generator/BuildPhaseGeneratorTests.swift index 1af5177f74f..b49192d0109 100644 --- a/Tests/TuistGeneratorTests/Generator/BuildPhaseGeneratorTests.swift +++ b/Tests/TuistGeneratorTests/Generator/BuildPhaseGeneratorTests.swift @@ -508,10 +508,10 @@ final class BuildPhaseGeneratorTests: TuistUnitTestCase { project: .test(path: "/path", sourceRootPath: "/path", xcodeProjPath: "/path/Project.xcodeproj"), pbxproj: pbxproj ) - try files.forEach { + for file in files { try fileElements.generate( fileElement: GroupFileElement( - path: $0, + path: file, group: .group(name: "Project"), isReference: true ), @@ -556,10 +556,10 @@ final class BuildPhaseGeneratorTests: TuistUnitTestCase { project: .test(path: "/path", sourceRootPath: "/path", xcodeProjPath: "/path/Project.xcodeproj"), pbxproj: pbxproj ) - try files.forEach { + for file in files { try fileElements.generate( fileElement: GroupFileElement( - path: $0, + path: file, group: .group(name: "Project"), isReference: true ), @@ -718,10 +718,10 @@ final class BuildPhaseGeneratorTests: TuistUnitTestCase { let fileElements = ProjectFileElements() let pbxproj = PBXProj() - resources.forEach { + for resource in resources { let ref = PBXFileReference() pbxproj.add(object: ref) - fileElements.elements[$0.path] = ref + fileElements.elements[resource.path] = ref } let nativeTarget = PBXNativeTarget(name: "Test") @@ -754,7 +754,7 @@ final class BuildPhaseGeneratorTests: TuistUnitTestCase { let resourceBuildPhase = try XCTUnwrap(nativeTarget.buildPhases.first as? PBXResourcesBuildPhase) var buildFiles = try XCTUnwrap(resourceBuildPhase.files) - try buildFiles.forEach { buildFile in + for buildFile in buildFiles { // Explicitly exctracting the original path because it gets lost in translation for resource files let path = try XCTUnwrap(fileElements.elements.first(where: { $0.value === buildFile.file })).key @@ -913,7 +913,7 @@ final class BuildPhaseGeneratorTests: TuistUnitTestCase { let projectA = Project.test(path: "/path/a") let appExtension = Target.test(name: "AppExtension", product: .appExtension) let stickerPackExtension = Target.test(name: "StickerPackExtension", product: .stickerPackExtension) - let app = Target.test(name: "App", product: .app) + let app = Target.test(name: "App", destinations: [.iPhone, .iPad, .mac], product: .app) let pbxproj = PBXProj() let nativeTarget = PBXNativeTarget(name: "Test") let fileElements = createProductFileElements(for: [appExtension, stickerPackExtension]) @@ -925,19 +925,28 @@ final class BuildPhaseGeneratorTests: TuistUnitTestCase { app.name: app, ], ] + let appGraphDependency: GraphDependency = .target(name: app.name, path: projectA.path) + let stickerPackGraphDependency: GraphDependency = .target(name: stickerPackExtension.name, path: projectA.path) + let appExtensionGraphDependency: GraphDependency = .target(name: appExtension.name, path: projectA.path) + let dependencies: [GraphDependency: Set] = [ - .target(name: appExtension.name, path: projectA.path): Set(), - .target(name: stickerPackExtension.name, path: projectA.path): Set(), - .target(name: app.name, path: projectA.path): Set([ - .target(name: appExtension.name, path: projectA.path), - .target(name: stickerPackExtension.name, path: projectA.path), + appExtensionGraphDependency: Set(), + stickerPackGraphDependency: Set(), + appGraphDependency: Set([ + appExtensionGraphDependency, + stickerPackGraphDependency, ]), ] + let dependencyConditions: [GraphEdge: PlatformCondition] = [ + .init(from: appGraphDependency, to: stickerPackGraphDependency): .when([.ios])!, + ] + let graph = Graph.test( path: path, projects: [projectA.path: projectA], targets: targets, - dependencies: dependencies + dependencies: dependencies, + dependencyConditions: dependencyConditions ) let graphTraverser = GraphTraverser(graph: graph) @@ -959,6 +968,10 @@ final class BuildPhaseGeneratorTests: TuistUnitTestCase { "AppExtension", "StickerPackExtension", ]) + XCTAssertEqual(pbxBuildPhase?.files?.map(\.platformFilter), [ + nil, + "ios", + ]) XCTAssertEqual( pbxBuildPhase?.files?.compactMap { $0.settings as? [String: [String]] }, [ @@ -1006,7 +1019,7 @@ final class BuildPhaseGeneratorTests: TuistUnitTestCase { func test_generateWatchBuildPhase() throws { // Given - let app = Target.test(name: "App", platform: .iOS, product: .app) + let app = Target.test(name: "App", destinations: [.iPad, .iPhone, .mac], product: .app) let watchApp = Target.test(name: "WatchApp", platform: .watchOS, product: .watch2App) let project = Project.test() let pbxproj = PBXProj() @@ -1016,15 +1029,25 @@ final class BuildPhaseGeneratorTests: TuistUnitTestCase { let targets: [AbsolutePath: [String: Target]] = [ project.path: [app.name: app, watchApp.name: watchApp], ] + + let appGraphDependency: GraphDependency = .target(name: app.name, path: project.path) + let watchAppGraphDependency: GraphDependency = .target(name: watchApp.name, path: project.path) + let dependencies: [GraphDependency: Set] = [ - .target(name: watchApp.name, path: project.path): Set(), - .target(name: app.name, path: project.path): Set([.target(name: watchApp.name, path: project.path)]), + watchAppGraphDependency: Set(), + appGraphDependency: Set([watchAppGraphDependency]), ] + + let dependencyConditions: [GraphEdge: PlatformCondition] = [ + .init(from: appGraphDependency, to: watchAppGraphDependency): .when([.ios])!, + ] + let graph = Graph.test( path: project.path, projects: [project.path: project], targets: targets, - dependencies: dependencies + dependencies: dependencies, + dependencyConditions: dependencyConditions ) let graphTraverser = GraphTraverser(graph: graph) @@ -1042,6 +1065,7 @@ final class BuildPhaseGeneratorTests: TuistUnitTestCase { XCTAssertEqual(pbxBuildPhase.files?.compactMap { $0.file?.nameOrPath }, [ "WatchApp", ]) + XCTAssertEqual(pbxBuildPhase.files?.first?.platformFilter, "ios") XCTAssertEqual( pbxBuildPhase.files?.compactMap { $0.settings as? [String: [String]] }, [["ATTRIBUTES": ["RemoveHeadersOnCopy"]]] @@ -1050,7 +1074,7 @@ final class BuildPhaseGeneratorTests: TuistUnitTestCase { func test_generateWatchBuildPhase_watchApplication() throws { // Given - let app = Target.test(name: "App", platform: .iOS, product: .app) + let app = Target.test(name: "App", destinations: [.iPhone, .iPad, .mac], product: .app) let watchApp = Target.test(name: "WatchApp", platform: .watchOS, product: .app) let project = Project.test() let pbxproj = PBXProj() @@ -1060,15 +1084,24 @@ final class BuildPhaseGeneratorTests: TuistUnitTestCase { let targets: [AbsolutePath: [String: Target]] = [ project.path: [app.name: app, watchApp.name: watchApp], ] + let appGraphDependency: GraphDependency = .target(name: app.name, path: project.path) + let watchAppGraphDependency: GraphDependency = .target(name: watchApp.name, path: project.path) + let dependencies: [GraphDependency: Set] = [ - .target(name: watchApp.name, path: project.path): Set(), - .target(name: app.name, path: project.path): Set([.target(name: watchApp.name, path: project.path)]), + watchAppGraphDependency: Set(), + appGraphDependency: Set([watchAppGraphDependency]), + ] + + let dependencyConditions: [GraphEdge: PlatformCondition] = [ + .init(from: appGraphDependency, to: watchAppGraphDependency): .when([.ios])!, ] + let graph = Graph.test( path: project.path, projects: [project.path: project], targets: targets, - dependencies: dependencies + dependencies: dependencies, + dependencyConditions: dependencyConditions ) let graphTraverser = GraphTraverser(graph: graph) @@ -1087,6 +1120,7 @@ final class BuildPhaseGeneratorTests: TuistUnitTestCase { XCTAssertEqual(pbxBuildPhase.files?.compactMap { $0.file?.nameOrPath }, [ "WatchApp", ]) + XCTAssertEqual(pbxBuildPhase.files?.first?.platformFilter, "ios") XCTAssertEqual( pbxBuildPhase.files?.compactMap { $0.settings as? [String: [String]] }, [["ATTRIBUTES": ["RemoveHeadersOnCopy"]]] @@ -1391,7 +1425,7 @@ final class BuildPhaseGeneratorTests: TuistUnitTestCase { func test_generateEmbedAppClipsBuildPhase() throws { // Given - let app = Target.test(name: "App", product: .app) + let app = Target.test(name: "App", destinations: [.iPhone, .iPad, .mac], product: .app) let appClip = Target.test(name: "AppClip", product: .appClip) let project = Project.test() let pbxproj = PBXProj() @@ -1401,15 +1435,24 @@ final class BuildPhaseGeneratorTests: TuistUnitTestCase { let targets: [AbsolutePath: [String: Target]] = [ project.path: [app.name: app, appClip.name: appClip], ] + let appGraphDependency: GraphDependency = .target(name: app.name, path: project.path) + let appClipGraphDependency: GraphDependency = .target(name: appClip.name, path: project.path) + let dependencies: [GraphDependency: Set] = [ - .target(name: appClip.name, path: project.path): Set(), - .target(name: app.name, path: project.path): Set([.target(name: appClip.name, path: project.path)]), + appClipGraphDependency: Set(), + appGraphDependency: Set([appClipGraphDependency]), + ] + + let dependencyConditions: [GraphEdge: PlatformCondition] = [ + .init(from: appGraphDependency, to: appClipGraphDependency): .when([.ios])!, ] + let graph = Graph.test( path: project.path, projects: [project.path: project], targets: targets, - dependencies: dependencies + dependencies: dependencies, + dependencyConditions: dependencyConditions ) let graphTraverser = GraphTraverser(graph: graph) // When @@ -1427,6 +1470,7 @@ final class BuildPhaseGeneratorTests: TuistUnitTestCase { XCTAssertNotNil(pbxBuildPhase) XCTAssertTrue(pbxBuildPhase is PBXCopyFilesBuildPhase) XCTAssertEqual(pbxBuildPhase?.files?.compactMap { $0.file?.nameOrPath }, ["AppClip"]) + XCTAssertEqual(pbxBuildPhase?.files?.first?.platformFilter, "ios") XCTAssertEqual( pbxBuildPhase?.files?.compactMap { $0.settings as? [String: [String]] }, [["ATTRIBUTES": ["RemoveHeadersOnCopy"]]] @@ -1546,10 +1590,10 @@ final class BuildPhaseGeneratorTests: TuistUnitTestCase { private func createFileElements(for coreDataModels: [CoreDataModel]) -> ProjectFileElements { let fileElements = ProjectFileElements() - coreDataModels.forEach { model in + for model in coreDataModels { let versionGroup = XCVersionGroup(path: model.path.basename, name: model.path.basename) fileElements.elements[model.path] = versionGroup - model.versions.forEach { version in + for version in model.versions { let fileReference = PBXFileReference(name: version.basename, path: version.basename) fileElements.elements[version] = fileReference } diff --git a/Tests/TuistGeneratorTests/Generator/ConfigGeneratorTests.swift b/Tests/TuistGeneratorTests/Generator/ConfigGeneratorTests.swift index 5bc91460120..8c97b882156 100644 --- a/Tests/TuistGeneratorTests/Generator/ConfigGeneratorTests.swift +++ b/Tests/TuistGeneratorTests/Generator/ConfigGeneratorTests.swift @@ -750,7 +750,7 @@ final class ConfigGeneratorTests: TuistUnitTestCase { targetSettingsResult, .array([ "-load-plugin-executable", - "$BUILT_PRODUCTS_DIR/\(macroFramework.productNameWithExtension)/Macros/\(macroExecutable.productName)#\(macroExecutable.productName)", + "$BUILD_DIR/Debug$EFFECTIVE_PLATFORM_NAME/\(macroExecutable.productName)#\(macroExecutable.productName)", ]) ) } diff --git a/Tests/TuistGeneratorTests/Generator/LinkGeneratorTests.swift b/Tests/TuistGeneratorTests/Generator/LinkGeneratorTests.swift index acf9711d99a..0a9924cada6 100644 --- a/Tests/TuistGeneratorTests/Generator/LinkGeneratorTests.swift +++ b/Tests/TuistGeneratorTests/Generator/LinkGeneratorTests.swift @@ -203,7 +203,7 @@ final class LinkGeneratorTests: XCTestCase { } func test_generateEmbedPhase_doesNot_includesSymbols_when_testTarget() throws { - try Product.allCases.filter(\.testsBundle).forEach { product in + for product in Product.allCases.filter(\.testsBundle) { // Given var dependencies: Set = [] @@ -975,9 +975,8 @@ final class LinkGeneratorTests: XCTestCase { func test_generateLinks_generatesAShellScriptBuildPhase_when_targetIsAMacroFramework() throws { // Given - let projectSettings = Settings.default let app = Target.test(name: "app", platform: .iOS, product: .app) - let macroFramework = Target.test(name: "framework", platform: .macOS, product: .staticFramework) + let macroFramework = Target.test(name: "framework", platform: .iOS, product: .staticFramework) let macroExecutable = Target.test(name: "macro", platform: .macOS, product: .macro) let project = Project.test(targets: [app, macroFramework, macroExecutable]) @@ -1015,17 +1014,21 @@ final class LinkGeneratorTests: XCTestCase { .pbxTarget .buildPhases .compactMap { $0 as? PBXShellScriptBuildPhase } - .first(where: { $0.name() == "Copy Swift Macro executable into /Macros" }) + .first(where: { $0.name() == "Copy Swift Macro executable into $BUILT_PRODUCT_DIR" }) XCTAssertNotNil(buildPhase) let expectedScript = - "cp \"$SYMROOT/$CONFIGURATION/\(macroExecutable.productName)\" \"$BUILT_PRODUCTS_DIR/$FULL_PRODUCT_NAME/Macros/\(macroExecutable.productName)\"" + "if [[ -f \"$BUILD_DIR/$CONFIGURATION/macro\" && ! -f \"$BUILD_DIR/Debug$EFFECTIVE_PLATFORM_NAME/macro\" ]]; then\n mkdir -p \"$BUILD_DIR/Debug$EFFECTIVE_PLATFORM_NAME/\"\n cp \"$BUILD_DIR/$CONFIGURATION/macro\" \"$BUILD_DIR/Debug$EFFECTIVE_PLATFORM_NAME/macro\"\nfi" XCTAssertTrue(buildPhase?.shellScript?.contains(expectedScript) == true) - XCTAssertTrue(buildPhase?.inputPaths.contains("$SYMROOT/$CONFIGURATION/\(macroExecutable.productName)") == true) + XCTAssertTrue(buildPhase?.inputPaths.contains("$BUILD_DIR/$CONFIGURATION/\(macroExecutable.productName)") == true) XCTAssertTrue( buildPhase?.outputPaths - .contains("$BUILT_PRODUCTS_DIR/$FULL_PRODUCT_NAME/Macros/\(macroExecutable.productName)") == true + .contains("$BUILD_DIR/Debug-iphonesimulator/\(macroExecutable.productName)") == true + ) + XCTAssertTrue( + buildPhase?.outputPaths + .contains("$BUILD_DIR/Debug-iphoneos/\(macroExecutable.productName)") == true ) } @@ -1059,8 +1062,8 @@ final class LinkGeneratorTests: XCTestCase { func createProjectFileElements(for targets: [Target]) -> ProjectFileElements { let projectFileElements = ProjectFileElements() - targets.forEach { - projectFileElements.products[$0.name] = PBXFileReference(path: $0.productNameWithExtension) + for target in targets { + projectFileElements.products[target.name] = PBXFileReference(path: target.productNameWithExtension) } return projectFileElements @@ -1071,11 +1074,11 @@ final class LinkGeneratorTests: XCTestCase { projectPath: AbsolutePath ) -> ProjectFileElements { let projectFileElements = ProjectFileElements() - dependencies.forEach { dependency in + for dependency in dependencies { switch dependency { - case .xcframework(path: let path, infoPlist: _, primaryBinaryPath: _, linking: _, mergeable: _, _): - projectFileElements.elements[path] = PBXFileReference( - path: path.relative(to: projectPath).pathString + case let .xcframework(xcframework): + projectFileElements.elements[xcframework.path] = PBXFileReference( + path: xcframework.path.relative(to: projectPath).pathString ) default: fatalError("Scenarios not handled in this test stub") diff --git a/Tests/TuistGeneratorTests/Generator/ProjectFileElementsTests.swift b/Tests/TuistGeneratorTests/Generator/ProjectFileElementsTests.swift index 2c22b5618ec..be29a7c573a 100644 --- a/Tests/TuistGeneratorTests/Generator/ProjectFileElementsTests.swift +++ b/Tests/TuistGeneratorTests/Generator/ProjectFileElementsTests.swift @@ -247,9 +247,9 @@ final class ProjectFileElementsTests: TuistUnitTestCase { } // When - try elements.forEach { + for element in elements { try subject.generate( - fileElement: $0, + fileElement: element, groups: groups, pbxproj: pbxproj, sourceRootPath: temporaryPath @@ -298,9 +298,9 @@ final class ProjectFileElementsTests: TuistUnitTestCase { } // When - try elements.forEach { + for element in elements { try subject.generate( - fileElement: $0, + fileElement: element, groups: groups, pbxproj: pbxproj, sourceRootPath: temporaryPath @@ -349,9 +349,9 @@ final class ProjectFileElementsTests: TuistUnitTestCase { } // When - try elements.forEach { + for element in elements { try subject.generate( - fileElement: $0, + fileElement: element, groups: groups, pbxproj: pbxproj, sourceRootPath: temporaryPath @@ -792,7 +792,7 @@ final class ProjectFileElementsTests: TuistUnitTestCase { ) // Then - XCTAssertEqual(groups.compiled.flattenedChildren, [ + XCTAssertEqual(groups.frameworks.flattenedChildren, [ "ARKit.framework", ]) @@ -841,7 +841,7 @@ final class ProjectFileElementsTests: TuistUnitTestCase { ) // Then - XCTAssertEqual(groups.compiled.flattenedChildren, [ + XCTAssertEqual(groups.frameworks.flattenedChildren, [ "Test.framework", ]) diff --git a/Tests/TuistGeneratorTests/Generator/ProjectGroupsTests.swift b/Tests/TuistGeneratorTests/Generator/ProjectGroupsTests.swift index 4fab3dc453a..114e4263aca 100644 --- a/Tests/TuistGeneratorTests/Generator/ProjectGroupsTests.swift +++ b/Tests/TuistGeneratorTests/Generator/ProjectGroupsTests.swift @@ -74,10 +74,10 @@ final class ProjectGroupsTests: XCTestCase { XCTAssertNil(main.group(named: "Target")?.path) XCTAssertEqual(main.group(named: "Target")?.sourceTree, .group) - XCTAssertTrue(main.children.contains(subject.compiled)) - XCTAssertEqual(subject.compiled.name, "Compiled") - XCTAssertNil(subject.compiled.path) - XCTAssertEqual(subject.compiled.sourceTree, .group) + XCTAssertTrue(main.children.contains(subject.frameworks)) + XCTAssertEqual(subject.frameworks.name, "Frameworks") + XCTAssertNil(subject.frameworks.path) + XCTAssertEqual(subject.frameworks.sourceTree, .group) XCTAssertTrue(main.children.contains(subject.products)) XCTAssertEqual(subject.products.name, "Products") @@ -111,7 +111,7 @@ final class ProjectGroupsTests: XCTestCase { "B", "C", "A", - "Compiled", + "Frameworks", "Products", ]) } @@ -125,7 +125,7 @@ final class ProjectGroupsTests: XCTestCase { let got = try subject.targetFrameworks(target: "Test") XCTAssertEqual(got.name, "Test") XCTAssertEqual(got.sourceTree, .group) - XCTAssertTrue(subject.compiled.children.contains(got)) + XCTAssertTrue(subject.frameworks.children.contains(got)) } func test_projectGroup_unknownProjectGroups() throws { diff --git a/Tests/TuistGeneratorTests/Generator/SchemeDescriptorsGeneratorTests.swift b/Tests/TuistGeneratorTests/Generator/SchemeDescriptorsGeneratorTests.swift index 78fd1ffeeef..79b36a8fde6 100644 --- a/Tests/TuistGeneratorTests/Generator/SchemeDescriptorsGeneratorTests.swift +++ b/Tests/TuistGeneratorTests/Generator/SchemeDescriptorsGeneratorTests.swift @@ -1002,6 +1002,63 @@ final class SchemeDescriptorsGeneratorTests: XCTestCase { XCTAssertEqual(postBuildableReference.buildableIdentifier, "primary") } + func test_schemeTestAction_when_testsTarget_with_skippedTests() throws { + // Given + let target = Target.test(name: "App", product: .app) + let testTarget = Target.test(name: "AppTests", product: .unitTests) + let project = Project.test(targets: [target, testTarget]) + + let testAction = TestAction.test( + targets: [TestableTarget(target: TargetReference(projectPath: project.path, name: "AppTests"))], + arguments: nil, + skippedTests: ["AppTests/test_twoPlusTwo_isFour"] + ) + + let scheme = Scheme.test(name: "AppTests", testAction: testAction) + let generatedProjects = createGeneratedProjects(projects: [project]) + + let graph = Graph.test( + projects: [project.path: project], + targets: [ + project.path: [ + target.name: target, + testTarget.name: testTarget, + ], + ], + dependencies: [ + .target(name: testTarget.name, path: project.path): [ + .target(name: target.name, path: project.path), + ], + ] + ) + let graphTraverser = GraphTraverser(graph: graph) + + // When + let got = try subject.schemeTestAction( + scheme: scheme, + graphTraverser: graphTraverser, + rootPath: project.path, + generatedProjects: generatedProjects + ) + + // Then + let result = try XCTUnwrap(got) + XCTAssertEqual(result.buildConfiguration, "Debug") + XCTAssertEqual(result.selectedDebuggerIdentifier, "Xcode.DebuggerFoundation.Debugger.LLDB") + XCTAssertEqual(result.selectedLauncherIdentifier, "Xcode.DebuggerFoundation.Launcher.LLDB") + XCTAssertEqual(result.shouldUseLaunchSchemeArgsEnv, true) + XCTAssertNil(result.macroExpansion) + let testable = try XCTUnwrap(result.testables.first) + let buildableReference = testable.buildableReference + + XCTAssertEqual(testable.skipped, false) + XCTAssertEqual(buildableReference.referencedContainer, "container:Project.xcodeproj") + XCTAssertEqual(buildableReference.buildableName, "AppTests.xctest") + XCTAssertEqual(buildableReference.blueprintName, "AppTests") + XCTAssertEqual(buildableReference.buildableIdentifier, "primary") + XCTAssertEqual(testable.skippedTests, [XCScheme.TestItem(identifier: "AppTests/test_twoPlusTwo_isFour")]) + } + // MARK: - Launch Action Tests func test_schemeLaunchAction() throws { diff --git a/Tests/TuistGeneratorTests/Generator/TargetGeneratorTests.swift b/Tests/TuistGeneratorTests/Generator/TargetGeneratorTests.swift index f715238d6b1..5caf0938c7e 100644 --- a/Tests/TuistGeneratorTests/Generator/TargetGeneratorTests.swift +++ b/Tests/TuistGeneratorTests/Generator/TargetGeneratorTests.swift @@ -107,22 +107,39 @@ final class TargetGeneratorTests: XCTestCase { func test_generateTargetDependencies() throws { // Given - let targetA = Target.test(name: "TargetA") - let targetB = Target.test(name: "TargetB") + let targetA = Target.test( + name: "TargetA", + destinations: [.mac, .iPhone] + ) + let targetB = Target.test( + name: "TargetB", + destinations: [.mac, .iPhone] + ) + let targetC = Target.test(name: "TargetC") let nativeTargetA = createNativeTarget(for: targetA) let nativeTargetB = createNativeTarget(for: targetB) + let nativeTargetC = createNativeTarget(for: targetC) let graph = Graph.test( projects: [path: .test(path: path)], targets: [ path: [ targetA.name: targetA, targetB.name: targetB, + targetC.name: targetC, ], ], dependencies: [ .target(name: targetA.name, path: path): [ .target(name: targetB.name, path: path), + .target(name: targetC.name, path: path), ], + ], + dependencyConditions: [ + GraphEdge( + from: .target(name: targetA.name, path: path), + to: .target(name: targetC.name, path: path) + ): + try XCTUnwrap(.when([.ios])), ] ) let graphTraverser = GraphTraverser(graph: graph) @@ -130,18 +147,26 @@ final class TargetGeneratorTests: XCTestCase { // When try subject.generateTargetDependencies( path: path, - targets: [targetA, targetB], + targets: [targetA, targetB, targetC], nativeTargets: [ "TargetA": nativeTargetA, "TargetB": nativeTargetB, + "TargetC": nativeTargetC, ], graphTraverser: graphTraverser ) // Then - XCTAssertEqual(nativeTargetA.dependencies.map(\.name), [ - "TargetB", - ]) + let expected: [PBXTargetDependency] = [ + PBXTargetDependency(name: "TargetB"), + PBXTargetDependency(name: "TargetC", platformFilter: "ios"), + ] + + for (index, dependency) in nativeTargetA.dependencies.enumerated() { + XCTAssertEqual(dependency.name, expected[index].name) + XCTAssertEqual(dependency.platformFilter, expected[index].platformFilter) + XCTAssertEqual(dependency.platformFilters, expected[index].platformFilters) + } } func test_generateTarget_actions() throws { diff --git a/Tests/TuistGeneratorTests/Generator/WorkspaceStructureGeneratorTests.swift b/Tests/TuistGeneratorTests/Generator/WorkspaceStructureGeneratorTests.swift index 0a029747b59..821f30a9221 100644 --- a/Tests/TuistGeneratorTests/Generator/WorkspaceStructureGeneratorTests.swift +++ b/Tests/TuistGeneratorTests/Generator/WorkspaceStructureGeneratorTests.swift @@ -318,8 +318,8 @@ final class WorkspaceStructureGeneratorTests: XCTestCase { @discardableResult func createFolders(_ folders: [String]) throws -> [AbsolutePath] { let paths = folders.map { try! AbsolutePath(validating: $0) } - try paths.forEach { - try fileHandler.createFolder($0) + for path in paths { + try fileHandler.createFolder(path) } return paths } @@ -327,8 +327,8 @@ final class WorkspaceStructureGeneratorTests: XCTestCase { @discardableResult func createFiles(_ files: [String]) throws -> [AbsolutePath] { let paths = files.map { try! AbsolutePath(validating: $0) } - try paths.forEach { - try fileHandler.touch($0) + for path in paths { + try fileHandler.touch(path) } return paths } diff --git a/Tests/TuistGeneratorTests/Linter/StaticProductsGraphLinterTests.swift b/Tests/TuistGeneratorTests/Linter/StaticProductsGraphLinterTests.swift index cc10272c5e6..58c9a59935b 100644 --- a/Tests/TuistGeneratorTests/Linter/StaticProductsGraphLinterTests.swift +++ b/Tests/TuistGeneratorTests/Linter/StaticProductsGraphLinterTests.swift @@ -1247,6 +1247,48 @@ class StaticProductsGraphLinterTests: XCTestCase { XCTAssertEqual(results, []) } + func test_lint_whenMacros() throws { + // Given + let path: AbsolutePath = "/project" + let app = Target.test(name: "App") + let macroStaticFramework = Target.test(name: "MacroStaticFramework", product: .staticFramework) + let macroExecutable = Target.test(name: "Macro", product: .macro) + let swiftSyntax = Target.test(name: "SwiftSyntax", product: .staticLibrary) + + let project = Project + .test(targets: [app, macroStaticFramework, macroExecutable, swiftSyntax]) + + let appDependency = GraphDependency.target(name: app.name, path: path) + let macroStaticFrameworkDependency = GraphDependency.target(name: macroStaticFramework.name, path: path) + let macroExecutableDependency = GraphDependency.target(name: macroExecutable.name, path: path) + let swiftSyntaxDependency = GraphDependency.target(name: swiftSyntax.name, path: path) + + let dependencies: [GraphDependency: Set] = [ + appDependency: Set([macroStaticFrameworkDependency]), + macroStaticFrameworkDependency: Set([macroExecutableDependency]), + macroExecutableDependency: Set([swiftSyntaxDependency]), + ] + let graph = Graph.test( + path: path, + projects: [path: project], + targets: [path: [ + app.name: app, + macroStaticFramework.name: macroStaticFramework, + macroExecutable.name: macroExecutable, + swiftSyntax.name: swiftSyntax, + ]], + dependencies: dependencies + ) + let config = Config.test() + let graphTraverser = GraphTraverser(graph: graph) + + // When + let results = subject.lint(graphTraverser: graphTraverser, config: config) + + // Then + XCTAssertEqual(results, []) + } + // MARK: - Helpers private func warning(product node: String, type: String = "Target", linkedBy: [GraphDependency]) -> LintingIssue { diff --git a/Tests/TuistGeneratorTests/ProjectMappers/ExplicitDependencyGraphMapperTests.swift b/Tests/TuistGeneratorTests/ProjectMappers/ExplicitDependencyGraphMapperTests.swift new file mode 100644 index 00000000000..413aef83dfb --- /dev/null +++ b/Tests/TuistGeneratorTests/ProjectMappers/ExplicitDependencyGraphMapperTests.swift @@ -0,0 +1,239 @@ +import Foundation +import TuistGraph +import TuistSupport +import TuistSupportTesting +import XCTest +@testable import TuistGenerator + +final class ExplicitDependencyGraphMapperTests: TuistUnitTestCase { + private var subject: ExplicitDependencyGraphMapper! + + override public func setUp() { + super.setUp() + subject = ExplicitDependencyGraphMapper() + } + + override public func tearDown() { + subject = nil + super.tearDown() + } + + func test_map() async throws { + // Given + let projectAPath = fileHandler.currentPath.appending(component: "ProjectA") + let externalProjectBPath = fileHandler.currentPath.appending(component: "ProjectB") + let frameworkA: Target = .test( + name: "FrameworkA", + product: .framework, + dependencies: [ + .target(name: "DynamicLibraryB"), + .project(target: "ExternalFrameworkC", path: externalProjectBPath), + ] + ) + let dynamicLibraryB: Target = .test( + name: "DynamicLibraryB", + product: .dynamicLibrary, + productName: "DynamicLibraryB" + ) + let externalFrameworkC: Target = .test( + name: "ExternalFrameworkC", + product: .staticFramework, + productName: "ExternalFrameworkC" + ) + let graph = Graph.test( + projects: [ + projectAPath: .test( + targets: [ + .test( + name: "App", + product: .app, + dependencies: [ + .target(name: "FrameworkA"), + ] + ), + frameworkA, + dynamicLibraryB, + ] + ), + externalProjectBPath: .test( + targets: [ + externalFrameworkC, + ], + isExternal: true + ), + ], + targets: [ + projectAPath: [ + "FrameworkA": frameworkA, + "DynamicLibraryB": dynamicLibraryB, + ], + externalProjectBPath: [ + "ExternalFrameworkC": externalFrameworkC, + ], + ], + dependencies: [ + .target(name: "FrameworkA", path: projectAPath): [ + .target(name: "DynamicLibraryB", path: projectAPath), + .target(name: "ExternalFrameworkC", path: externalProjectBPath), + ], + ] + ) + + // When + let got = try await subject.map(graph: graph) + let copyScript = """ + if [[ -d "$FILE" && ! -d "$DESTINATION_FILE" ]]; then + ln -s "$FILE" "$DESTINATION_FILE" + fi + + if [[ -f "$FILE" && ! -f "$DESTINATION_FILE" ]]; then + ln -s "$FILE" "$DESTINATION_FILE" + fi + """ + + // Then + XCTAssertEqual( + got.0.projects[projectAPath]?.targets[0], + .test( + name: "App", + product: .app, + dependencies: [ + .target(name: "FrameworkA"), + ] + ) + ) + let gotFrameworkA = try XCTUnwrap(got.0.projects[projectAPath]?.targets[1]) + XCTAssertEqual( + gotFrameworkA.name, + "FrameworkA" + ) + XCTAssertEqual( + gotFrameworkA.product, + .framework + ) + XCTAssertEqual( + gotFrameworkA.settings?.baseDebug["BUILT_PRODUCTS_DIR"], + "$(CONFIGURATION_BUILD_DIR)$(TARGET_BUILD_SUBPATH)/$(PRODUCT_NAME)" + ) + XCTAssertEqual( + gotFrameworkA.settings?.baseDebug["TARGET_BUILD_DIR"], + "$(CONFIGURATION_BUILD_DIR)$(TARGET_BUILD_SUBPATH)/$(PRODUCT_NAME)" + ) + switch gotFrameworkA.settings?.baseDebug["FRAMEWORK_SEARCH_PATHS"] { + case let .array(array): + XCTAssertEqual( + Set(array), + Set( + [ + "$(CONFIGURATION_BUILD_DIR)$(TARGET_BUILD_SUBPATH)/DynamicLibraryB", + "$(CONFIGURATION_BUILD_DIR)$(TARGET_BUILD_SUBPATH)/ExternalFrameworkC", + ] + ) + ) + default: + XCTFail("Invalid case for FRAMEWORK_SEARCH_PATHS") + } + XCTAssertEqual( + gotFrameworkA.scripts, + [ + TargetScript( + name: "Copy Built Products for Explicit Dependencies", + order: .post, + script: .embedded(""" + # This script copies built products into the shared directory to be available for app and other targets that don't have scoped directories + # If you try to archive any of the configurations seen in the output paths, the operation will fail due to `Multiple commands produce` error + + FILE="$BUILD_DIR/Debug$EFFECTIVE_PLATFORM_NAME$TARGET_BUILD_SUBPATH/$PRODUCT_NAME/$PRODUCT_NAME.framework" + DESTINATION_FILE="$BUILD_DIR/Debug$EFFECTIVE_PLATFORM_NAME$TARGET_BUILD_SUBPATH/$PRODUCT_NAME.framework" + \(copyScript) + """), + inputPaths: [ + "$(BUILD_DIR)/Debug$(EFFECTIVE_PLATFORM_NAME)$(TARGET_BUILD_SUBPATH)/$(PRODUCT_NAME)/$(PRODUCT_NAME).framework", + ], + outputPaths: [ + "$(BUILD_DIR)/Debug$(EFFECTIVE_PLATFORM_NAME)$(TARGET_BUILD_SUBPATH)/$(PRODUCT_NAME).framework", + ] + ), + ] + ) + XCTAssertEqual( + gotFrameworkA.dependencies, + [ + .target(name: "DynamicLibraryB"), + .project(target: "ExternalFrameworkC", path: externalProjectBPath), + ] + ) + XCTAssertEqual( + got.0.projects[projectAPath]?.targets[2], + .test( + name: "DynamicLibraryB", + product: .dynamicLibrary, + settings: .test( + baseDebug: [ + "BUILT_PRODUCTS_DIR": "$(CONFIGURATION_BUILD_DIR)$(TARGET_BUILD_SUBPATH)/$(PRODUCT_NAME)", + "TARGET_BUILD_DIR": "$(CONFIGURATION_BUILD_DIR)$(TARGET_BUILD_SUBPATH)/$(PRODUCT_NAME)", + ] + ), + scripts: [ + TargetScript( + name: "Copy Built Products for Explicit Dependencies", + order: .post, + script: .embedded(""" + # This script copies built products into the shared directory to be available for app and other targets that don't have scoped directories + # If you try to archive any of the configurations seen in the output paths, the operation will fail due to `Multiple commands produce` error + + FILE="$BUILD_DIR/Debug$EFFECTIVE_PLATFORM_NAME$TARGET_BUILD_SUBPATH/$PRODUCT_NAME/$PRODUCT_NAME.swiftmodule" + DESTINATION_FILE="$BUILD_DIR/Debug$EFFECTIVE_PLATFORM_NAME$TARGET_BUILD_SUBPATH/$PRODUCT_NAME.swiftmodule" + \(copyScript) + """), + inputPaths: [ + "$(BUILD_DIR)/Debug$(EFFECTIVE_PLATFORM_NAME)$(TARGET_BUILD_SUBPATH)/$(PRODUCT_NAME)/$(PRODUCT_NAME).swiftmodule", + ], + outputPaths: [ + "$(BUILD_DIR)/Debug$(EFFECTIVE_PLATFORM_NAME)$(TARGET_BUILD_SUBPATH)/$(PRODUCT_NAME).swiftmodule", + ] + ), + ] + ) + ) + XCTAssertEqual( + got.0.projects[externalProjectBPath]?.targets, + [ + .test( + name: "ExternalFrameworkC", + product: .staticFramework, + productName: "ExternalFrameworkC", + settings: .test( + baseDebug: [ + "BUILT_PRODUCTS_DIR": "$(CONFIGURATION_BUILD_DIR)$(TARGET_BUILD_SUBPATH)/$(PRODUCT_NAME)", + "TARGET_BUILD_DIR": "$(CONFIGURATION_BUILD_DIR)$(TARGET_BUILD_SUBPATH)/$(PRODUCT_NAME)", + "FRAMEWORK_SEARCH_PATHS": [ + "$(CONFIGURATION_BUILD_DIR)$(TARGET_BUILD_SUBPATH)", + ], + ] + ), + scripts: [ + TargetScript( + name: "Copy Built Products for Explicit Dependencies", + order: .post, + script: .embedded(""" + # This script copies built products into the shared directory to be available for app and other targets that don't have scoped directories + # If you try to archive any of the configurations seen in the output paths, the operation will fail due to `Multiple commands produce` error + + FILE="$BUILD_DIR/Debug$EFFECTIVE_PLATFORM_NAME$TARGET_BUILD_SUBPATH/$PRODUCT_NAME/$PRODUCT_NAME.framework" + DESTINATION_FILE="$BUILD_DIR/Debug$EFFECTIVE_PLATFORM_NAME$TARGET_BUILD_SUBPATH/$PRODUCT_NAME.framework" + \(copyScript) + """), + inputPaths: [ + "$(BUILD_DIR)/Debug$(EFFECTIVE_PLATFORM_NAME)$(TARGET_BUILD_SUBPATH)/$(PRODUCT_NAME)/$(PRODUCT_NAME).framework", + ], + outputPaths: [ + "$(BUILD_DIR)/Debug$(EFFECTIVE_PLATFORM_NAME)$(TARGET_BUILD_SUBPATH)/$(PRODUCT_NAME).framework", + ] + ), + ] + ), + ] + ) + } +} diff --git a/Tests/TuistGraphTests/Graph/GraphDependencyTests.swift b/Tests/TuistGraphTests/Graph/GraphDependencyTests.swift index 17a785db001..9cc2e18b7f0 100644 --- a/Tests/TuistGraphTests/Graph/GraphDependencyTests.swift +++ b/Tests/TuistGraphTests/Graph/GraphDependencyTests.swift @@ -20,4 +20,26 @@ final class GraphDependencyTests: TuistUnitTestCase { // Then XCTAssertCodable(subject) } + + func test_isLinkable() { + XCTAssertFalse(GraphDependency.testMacro().isLinkable) + XCTAssertTrue(GraphDependency.testXCFramework().isLinkable) + XCTAssertTrue(GraphDependency.testFramework().isLinkable) + XCTAssertTrue(GraphDependency.testLibrary().isLinkable) + XCTAssertFalse(GraphDependency.testBundle().isLinkable) + XCTAssertTrue(GraphDependency.testPackageProduct().isLinkable) + XCTAssertTrue(GraphDependency.testTarget().isLinkable) + XCTAssertTrue(GraphDependency.testSDK().isLinkable) + } + + func test_isPrecompiledMacro() { + XCTAssertTrue(GraphDependency.testMacro().isPrecompiledMacro) + XCTAssertFalse(GraphDependency.testXCFramework().isPrecompiledMacro) + XCTAssertFalse(GraphDependency.testFramework().isPrecompiledMacro) + XCTAssertFalse(GraphDependency.testLibrary().isPrecompiledMacro) + XCTAssertFalse(GraphDependency.testBundle().isPrecompiledMacro) + XCTAssertFalse(GraphDependency.testPackageProduct().isPrecompiledMacro) + XCTAssertFalse(GraphDependency.testTarget().isPrecompiledMacro) + XCTAssertFalse(GraphDependency.testSDK().isPrecompiledMacro) + } } diff --git a/Tests/TuistGraphTests/Models/ProductTests.swift b/Tests/TuistGraphTests/Models/ProductTests.swift index 98eb1f619fa..525845965a7 100644 --- a/Tests/TuistGraphTests/Models/ProductTests.swift +++ b/Tests/TuistGraphTests/Models/ProductTests.swift @@ -107,7 +107,7 @@ final class ProductTests: XCTestCase { .extensionKitExtension, .macro, ] - Product.allCases.forEach { product in + for product in Product.allCases { if runnables.contains(product) { XCTAssertTrue(product.runnable) } else { @@ -117,7 +117,7 @@ final class ProductTests: XCTestCase { } func test_testsBundle() { - Product.allCases.forEach { product in + for product in Product.allCases { if product == .uiTests || product == .unitTests { XCTAssertTrue(product.testsBundle) } else { diff --git a/Tests/TuistIntegrationTests/Generator/StableStructureIntegrationTests.swift b/Tests/TuistIntegrationTests/Generator/StableStructureIntegrationTests.swift index ae69ec97053..6e8a57d4b80 100644 --- a/Tests/TuistIntegrationTests/Generator/StableStructureIntegrationTests.swift +++ b/Tests/TuistIntegrationTests/Generator/StableStructureIntegrationTests.swift @@ -21,7 +21,7 @@ final class StableXcodeProjIntegrationTests: TuistTestCase { var capturedUserSchemes = [[XCScheme]]() // When - try (0 ..< 10).forEach { _ in + for _ in 0 ..< 10 { let subject = DescriptorGenerator() let writer = XcodeProjWriter() let config = TestModelGenerator.WorkspaceConfig( diff --git a/Tests/TuistKitIntegrationTests/Commands/DumpServiceIntegrationTests.swift b/Tests/TuistKitIntegrationTests/Commands/DumpServiceIntegrationTests.swift index 60b493fc7b5..f40ea0fccfc 100644 --- a/Tests/TuistKitIntegrationTests/Commands/DumpServiceIntegrationTests.swift +++ b/Tests/TuistKitIntegrationTests/Commands/DumpServiceIntegrationTests.swift @@ -171,6 +171,7 @@ final class DumpServiceTests: TuistTestCase { }, "generationOptions": { "disablePackageVersionLocking": false, + "enforceExplicitDependencies": false, "resolveDependenciesWithSystemScm": false, "staticSideEffectsWarningTargets": { "all": { diff --git a/Tests/TuistKitTests/Generator/Mocks/MockGeneratorFactory.swift b/Tests/TuistKitTests/Generator/Mocks/MockGeneratorFactory.swift index 94895455119..9f6cc5f9c16 100644 --- a/Tests/TuistKitTests/Generator/Mocks/MockGeneratorFactory.swift +++ b/Tests/TuistKitTests/Generator/Mocks/MockGeneratorFactory.swift @@ -60,7 +60,7 @@ final class MockGeneratorFactory: GeneratorFactorying { var invokedDefaultCount = 0 var stubbedDefaultResult: Generating! - func `default`() -> Generating { + func `default`(config _: Config) -> Generating { invokedDefault = true invokedDefaultCount += 1 return stubbedDefaultResult diff --git a/Tests/TuistKitTests/Mappers/Factories/GraphMapperFactoryTests.swift b/Tests/TuistKitTests/Mappers/Factories/GraphMapperFactoryTests.swift index 02768409a11..6450c11c2d8 100644 --- a/Tests/TuistKitTests/Mappers/Factories/GraphMapperFactoryTests.swift +++ b/Tests/TuistKitTests/Mappers/Factories/GraphMapperFactoryTests.swift @@ -26,9 +26,31 @@ final class GraphMapperFactoryTests: TuistUnitTestCase { func test_default_contains_the_update_workspace_projects_graph_mapper() { // When - let got = subject.default() + let got = subject.default(config: .test()) // Then XCTAssertContainsElementOfType(got, UpdateWorkspaceProjectsGraphMapper.self) } + + func test_default_contains_the_explicit_dependency_graph_mapper_when_explcit_dependencies_are_enforced() { + // When + let got = subject.default( + config: .test( + generationOptions: .test( + enforceExplicitDependencies: true + ) + ) + ) + + // Then + XCTAssertContainsElementOfType(got, ExplicitDependencyGraphMapper.self) + } + + func test_default_does_not_contain_the_explicit_dependency_graph_mapper() { + // When + let got = subject.default(config: .test()) + + // Then + XCTAssertDoesntContainElementOfType(got, ExplicitDependencyGraphMapper.self) + } } diff --git a/Tests/TuistKitTests/ProjectEditor/Mocks/MockProjectEditorMapper.swift b/Tests/TuistKitTests/ProjectEditor/Mocks/MockProjectEditorMapper.swift index 61a7035e649..347fa9106d6 100644 --- a/Tests/TuistKitTests/ProjectEditor/Mocks/MockProjectEditorMapper.swift +++ b/Tests/TuistKitTests/ProjectEditor/Mocks/MockProjectEditorMapper.swift @@ -17,6 +17,7 @@ final class MockProjectEditorMapper: ProjectEditorMapping { destinationDirectory: AbsolutePath, configPath: AbsolutePath?, dependenciesPath: AbsolutePath?, + packageManifestPath: AbsolutePath?, projectManifests: [AbsolutePath], editablePluginManifests: [EditablePluginManifest], pluginProjectDescriptionHelpersModule: [ProjectDescriptionHelpersModule], @@ -35,6 +36,7 @@ final class MockProjectEditorMapper: ProjectEditorMapping { destinationDirectory: AbsolutePath, configPath: AbsolutePath?, dependenciesPath: AbsolutePath?, + packageManifestPath: AbsolutePath?, projectManifests: [AbsolutePath], editablePluginManifests: [EditablePluginManifest], pluginProjectDescriptionHelpersModule: [ProjectDescriptionHelpersModule], @@ -52,6 +54,7 @@ final class MockProjectEditorMapper: ProjectEditorMapping { destinationDirectory: destinationDirectory, configPath: configPath, dependenciesPath: dependenciesPath, + packageManifestPath: packageManifestPath, projectManifests: projectManifests, editablePluginManifests: editablePluginManifests, pluginProjectDescriptionHelpersModule: pluginProjectDescriptionHelpersModule, diff --git a/Tests/TuistKitTests/ProjectEditor/ProjectEditorMapperTests.swift b/Tests/TuistKitTests/ProjectEditor/ProjectEditorMapperTests.swift index c6516d1686e..9648af52d85 100644 --- a/Tests/TuistKitTests/ProjectEditor/ProjectEditorMapperTests.swift +++ b/Tests/TuistKitTests/ProjectEditor/ProjectEditorMapperTests.swift @@ -1,5 +1,6 @@ import Foundation import TSCBasic +import TSCUtility import TuistCore import TuistGraph import TuistGraphTesting @@ -11,16 +12,21 @@ import XCTest @testable import TuistSupportTesting final class ProjectEditorMapperTests: TuistUnitTestCase { - var subject: ProjectEditorMapper! + private var subject: ProjectEditorMapper! + private var swiftPackageManagerController: MockSwiftPackageManagerController! override func setUp() { super.setUp() system.swiftVersionStub = { "5.2" } developerEnvironment.stubbedArchitecture = .arm64 - subject = ProjectEditorMapper() + swiftPackageManagerController = MockSwiftPackageManagerController() + subject = ProjectEditorMapper( + swiftPackageManagerController: swiftPackageManagerController + ) } override func tearDown() { + swiftPackageManagerController = nil subject = nil super.tearDown() } @@ -31,6 +37,7 @@ final class ProjectEditorMapperTests: TuistUnitTestCase { let projectManifestPaths = [sourceRootPath].map { $0.appending(component: "Project.swift") } let configPath = sourceRootPath.appending(components: Constants.tuistDirectoryName, "Config.swift") let dependenciesPath = sourceRootPath.appending(components: Constants.tuistDirectoryName, "Dependencies.swift") + let packageManifestPath = sourceRootPath.appending(components: Constants.tuistDirectoryName, "Package.swift") let helperPaths = [sourceRootPath].map { $0.appending(component: "Project+Template.swift") } let templates = [sourceRootPath].map { $0.appending(component: "template") } let templateResource = [sourceRootPath].map { $0.appending(component: "template.stencil") } @@ -44,6 +51,10 @@ final class ProjectEditorMapperTests: TuistUnitTestCase { sourceRootPath.appending(component: "PluginTwo"), sourceRootPath.appending(component: "PluginThree"), ].map { EditablePluginManifest(name: $0.basename, path: $0) } + swiftPackageManagerController.getToolsVersionStub = { _ in + Version("5.5.0") + } + xcodeController.selectedStub = .success(.test(path: AbsolutePath("/Applications/Xcode.app"))) // When let graph = try subject.map( @@ -53,6 +64,7 @@ final class ProjectEditorMapperTests: TuistUnitTestCase { destinationDirectory: sourceRootPath, configPath: configPath, dependenciesPath: dependenciesPath, + packageManifestPath: packageManifestPath, projectManifests: projectManifestPaths, editablePluginManifests: pluginPaths, pluginProjectDescriptionHelpersModule: [], @@ -73,7 +85,7 @@ final class ProjectEditorMapperTests: TuistUnitTestCase { // Then XCTAssertEqual(graph.name, "TestManifests") - XCTAssertEqual(targets.count, 9) + XCTAssertEqual(targets.count, 10) // Generated Manifests target let manifestsTarget = try XCTUnwrap(project.targets.first(where: { $0.name == sourceRootPath.basename + projectName })) @@ -198,6 +210,40 @@ final class ProjectEditorMapperTests: TuistUnitTestCase { .target(name: "PluginThree"), ])) + // Generated Packages target + let packagesTarget = try XCTUnwrap(project.targets.last(where: { $0.name == "Packages" })) + XCTAssertTrue(targets.contains(packagesTarget)) + + XCTAssertEqual(packagesTarget.name, "Packages") + XCTAssertEqual(packagesTarget.destinations, .macOS) + XCTAssertEqual(packagesTarget.product, .staticFramework) + var expectedPackagesSettings = expectedSettings(includePaths: [ + projectDescriptionPath, + projectDescriptionPath.parentDirectory, + ]) + expectedPackagesSettings = expectedPackagesSettings.with( + base: expectedPackagesSettings.base.merging( + [ + "OTHER_SWIFT_FLAGS": .array([ + "-package-description-version", + "5.5.0", + "-D", "TUIST", + ]), + "SWIFT_INCLUDE_PATHS": .array([ + "/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/pm/ManifestAPI", + ]), + ], + uniquingKeysWith: { $1 } + ) + ) + XCTAssertEqual( + packagesTarget.settings, + expectedPackagesSettings + ) + XCTAssertEqual(packagesTarget.dependencies, [.target(name: "ProjectDescriptionHelpers")]) + XCTAssertEqual(packagesTarget.sources.map(\.path), [packageManifestPath]) + XCTAssertEqual(packagesTarget.filesGroup, projectsGroup) + // Generated Project XCTAssertEqual(project.path, sourceRootPath.appending(component: projectName)) XCTAssertEqual(project.name, projectName) @@ -243,6 +289,7 @@ final class ProjectEditorMapperTests: TuistUnitTestCase { destinationDirectory: sourceRootPath, configPath: nil, dependenciesPath: nil, + packageManifestPath: nil, projectManifests: projectManifestPaths, editablePluginManifests: [], pluginProjectDescriptionHelpersModule: [], @@ -327,6 +374,7 @@ final class ProjectEditorMapperTests: TuistUnitTestCase { destinationDirectory: sourceRootPath, configPath: configPath, dependenciesPath: dependenciesPath, + packageManifestPath: nil, projectManifests: projectManifestPaths, editablePluginManifests: [], pluginProjectDescriptionHelpersModule: [], @@ -437,6 +485,7 @@ final class ProjectEditorMapperTests: TuistUnitTestCase { destinationDirectory: sourceRootPath, configPath: nil, dependenciesPath: nil, + packageManifestPath: nil, projectManifests: [], editablePluginManifests: editablePluginManifests, pluginProjectDescriptionHelpersModule: [], @@ -519,6 +568,7 @@ final class ProjectEditorMapperTests: TuistUnitTestCase { destinationDirectory: sourceRootPath, configPath: nil, dependenciesPath: nil, + packageManifestPath: nil, projectManifests: [], editablePluginManifests: editablePluginManifests, pluginProjectDescriptionHelpersModule: [], @@ -635,6 +685,7 @@ final class ProjectEditorMapperTests: TuistUnitTestCase { destinationDirectory: sourceRootPath, configPath: nil, dependenciesPath: nil, + packageManifestPath: nil, projectManifests: [], editablePluginManifests: editablePluginManifests, pluginProjectDescriptionHelpersModule: [], @@ -681,6 +732,7 @@ final class ProjectEditorMapperTests: TuistUnitTestCase { destinationDirectory: sourceRootPath, configPath: nil, dependenciesPath: nil, + packageManifestPath: nil, projectManifests: projectManifestPaths, editablePluginManifests: [localPlugin], pluginProjectDescriptionHelpersModule: [remotePlugin], diff --git a/Tests/TuistKitTests/ProjectEditor/ProjectEditorTests.swift b/Tests/TuistKitTests/ProjectEditor/ProjectEditorTests.swift index 67cf51e3e2b..4f9a5e308f8 100644 --- a/Tests/TuistKitTests/ProjectEditor/ProjectEditorTests.swift +++ b/Tests/TuistKitTests/ProjectEditor/ProjectEditorTests.swift @@ -95,7 +95,8 @@ final class ProjectEditorTests: TuistUnitTestCase { ] let tuistPath = try AbsolutePath(validating: ProcessInfo.processInfo.arguments.first!) let configPath = directory.appending(components: "Tuist", "Config.swift") - let dependenciesPath = directory.appending(components: "Tuist", "Dependencies.swif") + let dependenciesPath = directory.appending(components: "Tuist", "Dependencies.swift") + let packageManifestPath = directory.appending(components: "Tuist", "Package.swift") try FileHandler.shared.createFolder(directory.appending(component: "a folder")) try FileHandler.shared.write( """ @@ -121,6 +122,7 @@ final class ProjectEditorTests: TuistUnitTestCase { } manifestFilesLocator.locateConfigStub = configPath manifestFilesLocator.locateDependenciesStub = dependenciesPath + manifestFilesLocator.locatePackageManifestStub = packageManifestPath helpersDirectoryLocator.locateStub = helpersDirectory projectEditorMapper.mapStub = graph generator.generateWorkspaceStub = { _ in @@ -139,6 +141,7 @@ final class ProjectEditorTests: TuistUnitTestCase { XCTAssertEqual(mapArgs?.projectDescriptionPath, projectDescriptionPath.parentDirectory) XCTAssertEqual(mapArgs?.configPath, configPath) XCTAssertEqual(mapArgs?.dependenciesPath, dependenciesPath) + XCTAssertEqual(mapArgs?.packageManifestPath, packageManifestPath) } func test_edit_when_there_are_no_editable_files() throws { diff --git a/Tests/TuistKitTests/Services/BuildServiceTests.swift b/Tests/TuistKitTests/Services/BuildServiceTests.swift index 15846cfe2ab..16abed287e2 100644 --- a/Tests/TuistKitTests/Services/BuildServiceTests.swift +++ b/Tests/TuistKitTests/Services/BuildServiceTests.swift @@ -97,13 +97,14 @@ final class BuildServiceTests: TuistUnitTestCase { XCTAssertEqual(_skipSigning, skipSigning) return buildArguments } - targetBuilder.buildTargetStub = { _, _workspacePath, _scheme, _clean, _, _, _, _device, _osVersion, _, _, _ in - XCTAssertEqual(_workspacePath, workspacePath) - XCTAssertEqual(_scheme, scheme) - XCTAssertTrue(_clean) - XCTAssertNil(_device) - XCTAssertNil(_osVersion) - } + targetBuilder + .buildTargetStub = { _, _workspacePath, _scheme, _clean, _, _, _, _device, _osVersion, _, _ in + XCTAssertEqual(_workspacePath, workspacePath) + XCTAssertEqual(_scheme, scheme) + XCTAssertTrue(_clean) + XCTAssertNil(_device) + XCTAssertNil(_osVersion) + } // Then try await subject.testRun( @@ -144,7 +145,7 @@ final class BuildServiceTests: TuistUnitTestCase { XCTAssertEqual(_skipSigning, skipSigning) return buildArguments } - targetBuilder.buildTargetStub = { _, _workspacePath, _scheme, _clean, _, _, _, _, _, _, _, _ in + targetBuilder.buildTargetStub = { _, _workspacePath, _scheme, _clean, _, _, _, _, _, _, _ in XCTAssertEqual(_workspacePath, workspacePath) XCTAssertEqual(_scheme, scheme) XCTAssertTrue(_clean) @@ -190,22 +191,23 @@ final class BuildServiceTests: TuistUnitTestCase { XCTAssertEqual(_skipSigning, skipSigning) return buildArguments } - targetBuilder.buildTargetStub = { _, _workspacePath, _scheme, _clean, _, _, _, _device, _osVersion, _, _, _ in - XCTAssertEqual(_workspacePath, workspacePath) - XCTAssertNil(_device) - XCTAssertNil(_osVersion) + targetBuilder + .buildTargetStub = { _, _workspacePath, _scheme, _clean, _, _, _, _device, _osVersion, _, _ in + XCTAssertEqual(_workspacePath, workspacePath) + XCTAssertNil(_device) + XCTAssertNil(_osVersion) - if _scheme.name == "A" { - XCTAssertEqual(_scheme, schemeA) - XCTAssertTrue(_clean) - } else if _scheme.name == "B" { - // When running the second scheme clean should be false - XCTAssertEqual(_scheme, schemeB) - XCTAssertFalse(_clean) - } else { - XCTFail("unexpected scheme \(_scheme.name)") + if _scheme.name == "A" { + XCTAssertEqual(_scheme, schemeA) + XCTAssertTrue(_clean) + } else if _scheme.name == "B" { + // When running the second scheme clean should be false + XCTAssertEqual(_scheme, schemeB) + XCTAssertFalse(_clean) + } else { + XCTFail("unexpected scheme \(_scheme.name)") + } } - } // Then try await subject.testRun( @@ -246,9 +248,8 @@ final class BuildServiceTests: TuistUnitTestCase { XCTAssertEqual(_skipSigning, skipSigning) return buildArguments } - targetBuilder.buildTargetStub = { _, _workspacePath, _scheme, _clean, _, _, _, _, _, _, _, _ in + targetBuilder.buildTargetStub = { _, _workspacePath, _scheme, _clean, _, _, _, _, _, _, _ in XCTAssertEqual(_workspacePath, workspacePath) - if _scheme.name == "A" { XCTAssertEqual(_scheme, schemeA) XCTAssertTrue(_clean) @@ -310,7 +311,8 @@ extension BuildService { device: String? = nil, platform: String? = nil, osVersion: String? = nil, - rosetta: Bool = false + rosetta: Bool = false, + generateOnly: Bool = false ) async throws { try await run( schemeName: schemeName, @@ -324,7 +326,7 @@ extension BuildService { platform: platform, osVersion: osVersion, rosetta: rosetta, - rawXcodebuildLogs: false + generateOnly: generateOnly ) } } diff --git a/Tests/TuistKitTests/Services/CleanServiceTests.swift b/Tests/TuistKitTests/Services/CleanServiceTests.swift index c00a913de65..4dcec31830c 100644 --- a/Tests/TuistKitTests/Services/CleanServiceTests.swift +++ b/Tests/TuistKitTests/Services/CleanServiceTests.swift @@ -28,7 +28,7 @@ final class CleanServiceTests: TuistUnitTestCase { func test_run_with_category_cleans_category() throws { // Given - let cachePaths = try createFolders(["Cache", "Cache/BuildCache", "Cache/Manifests", "Cache/TestsCache"]) + let cachePaths = try createFolders(["Cache", "Cache/BuildCache", "Cache/Manifests", "Cache/incremental-tests"]) let cachePath = cachePaths[0] for path in cachePaths { let correctlyCreated = FileManager.default.fileExists(atPath: path.pathString) @@ -53,7 +53,7 @@ final class CleanServiceTests: TuistUnitTestCase { func test_run_without_category_cleans_all() throws { // Given - let cachePaths = try createFolders(["Cache", "Cache/BuildCache", "Cache/Manifests", "Cache/TestsCache"]) + let cachePaths = try createFolders(["Cache", "Cache/BuildCache", "Cache/Manifests", "Cache/incremental-tests"]) let cachePath = cachePaths[0] for path in cachePaths { let correctlyCreated = FileManager.default.fileExists(atPath: path.pathString) diff --git a/Tests/TuistKitTests/Services/FetchServiceTests.swift b/Tests/TuistKitTests/Services/FetchServiceTests.swift index 845904e92e0..1ba83568f4c 100644 --- a/Tests/TuistKitTests/Services/FetchServiceTests.swift +++ b/Tests/TuistKitTests/Services/FetchServiceTests.swift @@ -20,6 +20,7 @@ final class FetchServiceTests: TuistUnitTestCase { private var configLoader: MockConfigLoader! private var manifestLoader: MockManifestLoader! private var dependenciesController: MockDependenciesController! + private var packageSettingsLoader: MockPackageSettingsLoader! private var dependenciesModelLoader: MockDependenciesModelLoader! private var subject: FetchService! @@ -33,13 +34,15 @@ final class FetchServiceTests: TuistUnitTestCase { manifestLoader.manifestsAtStub = { _ in [.project] } dependenciesController = MockDependenciesController() dependenciesModelLoader = MockDependenciesModelLoader() + packageSettingsLoader = MockPackageSettingsLoader() subject = FetchService( pluginService: pluginService, configLoader: configLoader, manifestLoader: manifestLoader, dependenciesController: dependenciesController, - dependenciesModelLoader: dependenciesModelLoader + dependenciesModelLoader: dependenciesModelLoader, + packageSettingsLoader: packageSettingsLoader ) } @@ -77,7 +80,7 @@ final class FetchServiceTests: TuistUnitTestCase { let stubbedSwiftVersion = TSCUtility.Version(5, 3, 0) configLoader.loadConfigStub = { _ in Config.test(swiftVersion: stubbedSwiftVersion) } - dependenciesController.updateStub = { path, dependencies, swiftVersion in + dependenciesController.legacyUpdateStub = { path, dependencies, swiftVersion in XCTAssertEqual(path, stubbedPath) XCTAssertEqual(dependencies, stubbedDependencies) XCTAssertEqual(swiftVersion, stubbedSwiftVersion) @@ -161,7 +164,7 @@ final class FetchServiceTests: TuistUnitTestCase { let stubbedSwiftVersion = TSCUtility.Version(5, 3, 0) configLoader.loadConfigStub = { _ in Config.test(swiftVersion: stubbedSwiftVersion) } - dependenciesController.fetchStub = { path, dependencies, swiftVersion in + dependenciesController.legacyFetchStub = { path, dependencies, swiftVersion in XCTAssertEqual(path, stubbedPath) XCTAssertEqual(dependencies, stubbedDependencies) XCTAssertEqual(swiftVersion, stubbedSwiftVersion) @@ -192,4 +195,104 @@ final class FetchServiceTests: TuistUnitTestCase { XCTAssertFalse(dependenciesController.invokedUpdate) } + + func test_fetch_when_from_a_tuist_project_directory() async throws { + // Given + let exp = expectation(description: "awaiting path validation") + let temporaryDirectory = try temporaryPath() + let expectedFoundDependenciesLocation = temporaryDirectory.appending( + components: Constants.tuistDirectoryName, Manifest.dependencies.fileName(temporaryDirectory) + ) + let stubbedDependencies = Dependencies( + carthage: nil, + swiftPackageManager: nil, + platforms: [.iOS, .macOS] + ) + + // When looking for the Dependencies.swift file the model loader will search in the given path + // This is where we will assert + dependenciesModelLoader.loadDependenciesStub = { path, _ in + defer { exp.fulfill() } + XCTAssertEqual(temporaryDirectory, path) + return stubbedDependencies + } + + // Dependencies.swift in root + try fileHandler.touch(expectedFoundDependenciesLocation) + + // When - This will cause the `loadDependenciesStub` closure to be called and assert if needed + try await subject.run( + path: temporaryDirectory.pathString, + update: false + ) + await fulfillment(of: [exp], timeout: 0.1) + } + + func test_fetch_path_is_found_in_tuist_project_directory_but_manifest_is_in_nested_directory() async throws { + // Given + let exp = expectation(description: "awaiting path validation") + let temporaryDirectory = try temporaryPath() + let manifestPath = temporaryDirectory + .appending(components: ["First", "Second"]) + let expectedFoundDependenciesLocation = temporaryDirectory.appending( + components: Constants.tuistDirectoryName, Manifest.dependencies.fileName(temporaryDirectory) + ) + let stubbedDependencies = Dependencies( + carthage: nil, + swiftPackageManager: nil, + platforms: [.iOS, .macOS] + ) + + // When looking for the Dependencies.swift file the model loader will search in the given path + // This is where we will assert + dependenciesModelLoader.loadDependenciesStub = { path, _ in + defer { exp.fulfill() } + XCTAssertEqual(temporaryDirectory, path) + return stubbedDependencies + } + + // Dependencies.swift in root + try fileHandler.touch(expectedFoundDependenciesLocation) + + // When - This will cause the `loadDependenciesStub` closure to be called and assert if needed + try await subject.run( + path: manifestPath.pathString, + update: false + ) + await fulfillment(of: [exp], timeout: 0.1) + } + + func test_fetch_path_is_found_in_nested_manifest_directory() async throws { + // Given + let exp = expectation(description: "awaiting path validation") + let temporaryDirectory = try temporaryPath() + let manifestPath = temporaryDirectory + .appending(components: ["First", "Second"]) + let expectedFoundDependenciesLocation = manifestPath.appending( + components: Constants.tuistDirectoryName, Manifest.dependencies.fileName(temporaryDirectory) + ) + let stubbedDependencies = Dependencies( + carthage: nil, + swiftPackageManager: nil, + platforms: [.iOS, .macOS] + ) + + // When looking for the Dependencies.swift file the model loader will search in the given path + // This is where we will assert + dependenciesModelLoader.loadDependenciesStub = { path, _ in + defer { exp.fulfill() } + XCTAssertEqual(manifestPath, path) + return stubbedDependencies + } + + // Dependencies.swift in root + try fileHandler.touch(expectedFoundDependenciesLocation) + + // When - This will cause the `loadDependenciesStub` closure to be called and assert if needed + try await subject.run( + path: manifestPath.pathString, + update: false + ) + await fulfillment(of: [exp], timeout: 0.1) + } } diff --git a/Tests/TuistKitTests/Services/RunServiceTests.swift b/Tests/TuistKitTests/Services/RunServiceTests.swift index fa827abb3e1..c69d4455d2a 100644 --- a/Tests/TuistKitTests/Services/RunServiceTests.swift +++ b/Tests/TuistKitTests/Services/RunServiceTests.swift @@ -112,14 +112,15 @@ final class RunServiceTests: TuistUnitTestCase { let schemeName = "AScheme" let clean = true let configuration = "Test" - targetBuilder.buildTargetStub = { _, _workspacePath, _scheme, _clean, _configuration, _, _, _, _, _, _, _ in - // Then - XCTAssertEqual(_workspacePath, workspacePath) - XCTAssertEqual(_scheme.name, schemeName) - XCTAssertEqual(_clean, clean) - XCTAssertEqual(_configuration, configuration) - expectation.fulfill() - } + targetBuilder + .buildTargetStub = { _, _workspacePath, _scheme, _clean, _configuration, _, _, _, _, _, _ in + // Then + XCTAssertEqual(_workspacePath, workspacePath) + XCTAssertEqual(_scheme.name, schemeName) + XCTAssertEqual(_clean, clean) + XCTAssertEqual(_configuration, configuration) + expectation.fulfill() + } generator.generateWithGraphStub = { _ in (workspacePath, .test()) } targetRunner.assertCanRunTargetStub = { _ in } buildGraphInspector.workspacePathStub = { _ in workspacePath } @@ -184,7 +185,7 @@ final class RunServiceTests: TuistUnitTestCase { buildGraphInspector.workspacePathStub = { _ in workspacePath } buildGraphInspector.runnableSchemesStub = { _ in [.test()] } buildGraphInspector.runnableTargetStub = { _, _ in .test() } - targetBuilder.buildTargetStub = { _, _, _, _, _, _, _, _, _, _, _, _ in expectation.fulfill() } + targetBuilder.buildTargetStub = { _, _, _, _, _, _, _, _, _, _, _ in expectation.fulfill() } targetRunner.assertCanRunTargetStub = { _ in throw TestError() } // Then @@ -217,8 +218,7 @@ extension RunService { device: device, version: version, rosetta: rosetta, - arguments: arguments, - rawXcodebuildLogs: false + arguments: arguments ) } } diff --git a/Tests/TuistKitTests/Services/TestServiceTests.swift b/Tests/TuistKitTests/Services/TestServiceTests.swift index e9d3a6b648e..85d1bf62120 100644 --- a/Tests/TuistKitTests/Services/TestServiceTests.swift +++ b/Tests/TuistKitTests/Services/TestServiceTests.swift @@ -203,7 +203,7 @@ final class TestServiceTests: TuistUnitTestCase { (path, Graph.test()) } var testedRosetta: Bool? - xcodebuildController.testStub = { _, _, _, _, rosetta, _, _, _, _, _, _, _, _ in + xcodebuildController.testStub = { _, _, _, _, rosetta, _, _, _, _, _, _, _ in testedRosetta = rosetta return [.standardOutput(.init(raw: "success"))] } @@ -238,7 +238,7 @@ final class TestServiceTests: TuistUnitTestCase { (path, Graph.test()) } var testedSchemes: [String] = [] - xcodebuildController.testStub = { _, scheme, _, _, _, _, _, _, _, _, _, _, _ in + xcodebuildController.testStub = { _, scheme, _, _, _, _, _, _, _, _, _, _ in testedSchemes.append(scheme) return [.standardOutput(.init(raw: "success"))] } @@ -270,7 +270,7 @@ final class TestServiceTests: TuistUnitTestCase { (path, Graph.test()) } var testedSchemes: [String] = [] - xcodebuildController.testStub = { _, scheme, _, _, _, _, _, _, _, _, _, _, _ in + xcodebuildController.testStub = { _, scheme, _, _, _, _, _, _, _, _, _, _ in testedSchemes.append(scheme) return [.standardOutput(.init(raw: "success"))] } @@ -294,12 +294,6 @@ final class TestServiceTests: TuistUnitTestCase { "ProjectSchemeTwo", ] ) - XCTAssertTrue( - fileHandler.exists(cacheDirectoriesProvider.cacheDirectory(for: .tests).appending(component: "A")) - ) - XCTAssertTrue( - fileHandler.exists(cacheDirectoriesProvider.cacheDirectory(for: .tests).appending(component: "B")) - ) } func test_run_tests_individual_scheme() async throws { @@ -319,7 +313,7 @@ final class TestServiceTests: TuistUnitTestCase { (path, Graph.test()) } var testedSchemes: [String] = [] - xcodebuildController.testStub = { _, scheme, _, _, _, _, _, _, _, _, _, _, _ in + xcodebuildController.testStub = { _, scheme, _, _, _, _, _, _, _, _, _, _ in testedSchemes.append(scheme) return [.standardOutput(.init(raw: "success"))] } @@ -338,12 +332,34 @@ final class TestServiceTests: TuistUnitTestCase { // Then XCTAssertEqual(testedSchemes, ["ProjectSchemeOne"]) - XCTAssertTrue( - fileHandler.exists(cacheDirectoriesProvider.cacheDirectory(for: .tests).appending(component: "A")) - ) - XCTAssertTrue( - fileHandler.exists(cacheDirectoriesProvider.cacheDirectory(for: .tests).appending(component: "B")) + } + + func test_run_tests_with_skipped_targets() async throws { + // Given + buildGraphInspector.testableSchemesStub = { _ in + [ + Scheme.test(name: "ProjectSchemeOneTests"), + ] + } + generator.generateWithGraphStub = { path in + (path, Graph.test()) + } + var testedSchemes: [String] = [] + xcodebuildController.testStub = { _, scheme, _, _, _, _, _, _, _, _, _, _ in + testedSchemes.append(scheme) + return [.standardOutput(.init(raw: "success"))] + } + + // When + try await subject.testRun( + schemeName: "ProjectSchemeOneTests", + path: try temporaryPath(), + skipTestTargets: [.init(target: "ProjectSchemeOnTests", class: "TestClass")] ) + + // Then + XCTAssertEqual(testedSchemes, ["ProjectSchemeOneTests"]) + XCTAssertEqual(generatorFactory.invokedTestParameters?.excludedTargets, []) } func test_run_tests_all_project_schemes_when_fails() async throws { @@ -358,7 +374,7 @@ final class TestServiceTests: TuistUnitTestCase { } var testedSchemes: [String] = [] xcodebuildController.testErrorStub = NSError.test() - xcodebuildController.testStub = { _, scheme, _, _, _, _, _, _, _, _, _, _, _ in + xcodebuildController.testStub = { _, scheme, _, _, _, _, _, _, _, _, _, _ in testedSchemes.append(scheme) return [] } @@ -393,7 +409,7 @@ final class TestServiceTests: TuistUnitTestCase { (path, Graph.test()) } var testedSchemes: [String] = [] - xcodebuildController.testStub = { _, scheme, _, _, _, _, _, _, _, _, _, _, _ in + xcodebuildController.testStub = { _, scheme, _, _, _, _, _, _, _, _, _, _ in testedSchemes.append(scheme) return [.standardOutput(.init(raw: "success"))] } @@ -413,7 +429,7 @@ final class TestServiceTests: TuistUnitTestCase { let expectedResourceBundlePath = try AbsolutePath(validating: "/test") var resourceBundlePath: AbsolutePath? - xcodebuildController.testStub = { _, _, _, _, _, _, gotResourceBundlePath, _, _, _, _, _, _ in + xcodebuildController.testStub = { _, _, _, _, _, _, gotResourceBundlePath, _, _, _, _, _ in resourceBundlePath = gotResourceBundlePath return [] } @@ -444,7 +460,7 @@ final class TestServiceTests: TuistUnitTestCase { let expectedResourceBundlePath = try AbsolutePath(validating: "/test") var resourceBundlePath: AbsolutePath? - xcodebuildController.testStub = { _, _, _, _, _, _, gotResourceBundlePath, _, _, _, _, _, _ in + xcodebuildController.testStub = { _, _, _, _, _, _, gotResourceBundlePath, _, _, _, _, _ in resourceBundlePath = gotResourceBundlePath return [] } @@ -489,7 +505,7 @@ final class TestServiceTests: TuistUnitTestCase { } var passedRetryCount = 0 - xcodebuildController.testStub = { _, _, _, _, _, _, _, _, retryCount, _, _, _, _ in + xcodebuildController.testStub = { _, _, _, _, _, _, _, _, retryCount, _, _, _ in passedRetryCount = retryCount return [.standardOutput(.init(raw: "success"))] } @@ -522,7 +538,7 @@ final class TestServiceTests: TuistUnitTestCase { } var passedRetryCount = -1 - xcodebuildController.testStub = { _, _, _, _, _, _, _, _, retryCount, _, _, _, _ in + xcodebuildController.testStub = { _, _, _, _, _, _, _, _, retryCount, _, _, _ in passedRetryCount = retryCount return [.standardOutput(.init(raw: "success"))] } @@ -565,7 +581,7 @@ final class TestServiceTests: TuistUnitTestCase { (path, Graph.test()) } var testedSchemes: [String] = [] - xcodebuildController.testStub = { _, scheme, _, _, _, _, _, _, _, _, _, _, _ in + xcodebuildController.testStub = { _, scheme, _, _, _, _, _, _, _, _, _, _ in testedSchemes.append(scheme) return [.standardOutput(.init(raw: "success"))] } @@ -605,7 +621,7 @@ final class TestServiceTests: TuistUnitTestCase { generator.generateWithGraphStub = { path in (path, Graph.test()) } - xcodebuildController.testStub = { _, _, _, _, _, _, _, _, _, _, _, _, _ in + xcodebuildController.testStub = { _, _, _, _, _, _, _, _, _, _, _, _ in [.standardOutput(.init(raw: "success"))] } @@ -645,7 +661,8 @@ extension TestService { retryCount: Int = 0, testTargets: [TestIdentifier] = [], skipTestTargets: [TestIdentifier] = [], - testPlanConfiguration: TestPlanConfiguration? = nil + testPlanConfiguration: TestPlanConfiguration? = nil, + generateOnly: Bool = false ) async throws { try await run( schemeName: schemeName, @@ -663,7 +680,7 @@ extension TestService { testTargets: testTargets, skipTestTargets: skipTestTargets, testPlanConfiguration: testPlanConfiguration, - rawXcodebuildLogs: false + generateOnly: generateOnly ) } } diff --git a/Tests/TuistLoaderIntegrationTests/Loaders/ManifestLoaderTests.swift b/Tests/TuistLoaderIntegrationTests/Loaders/ManifestLoaderTests.swift index 507b86ba1a9..4cf9d026691 100644 --- a/Tests/TuistLoaderIntegrationTests/Loaders/ManifestLoaderTests.swift +++ b/Tests/TuistLoaderIntegrationTests/Loaders/ManifestLoaderTests.swift @@ -75,6 +75,91 @@ final class ManifestLoaderTests: TuistTestCase { XCTAssertEqual(got.name, "tuist") } + func test_loadPackageSettings() throws { + // Given + let temporaryPath = try temporaryPath() + let content = """ + // swift-tools-version: 5.9 + import PackageDescription + + #if TUIST + import ProjectDescription + + let packageSettings = PackageSettings( + platforms: [.iOS, .watchOS] + ) + + #endif + + let package = Package( + name: "PackageName", + dependencies: [ + .package(url: "https://github.com/Alamofire/Alamofire", exact: "5.8.0"), + ] + ) + + """ + + let manifestPath = temporaryPath.appending( + components: [ + Constants.tuistDirectoryName, + Manifest.package.fileName(temporaryPath), + ] + ) + try FileHandler.shared.createFolder(temporaryPath.appending(component: Constants.tuistDirectoryName)) + try content.write( + to: manifestPath.url, + atomically: true, + encoding: .utf8 + ) + + // When + let got = try subject.loadPackageSettings(at: temporaryPath) + + // Then + XCTAssertEqual( + got, + .init(platforms: [.iOS, .watchOS]) + ) + } + + func test_loadPackageSettings_without_package_settings() throws { + // Given + let temporaryPath = try temporaryPath() + let content = """ + // swift-tools-version: 5.9 + import PackageDescription + + let package = Package( + name: "PackageName", + dependencies: [] + ) + + """ + + let manifestPath = temporaryPath.appending( + components: [ + Constants.tuistDirectoryName, + Manifest.package.fileName(temporaryPath), + ] + ) + try FileHandler.shared.createFolder(temporaryPath.appending(component: Constants.tuistDirectoryName)) + try content.write( + to: manifestPath.url, + atomically: true, + encoding: .utf8 + ) + + // When + let got = try subject.loadPackageSettings(at: temporaryPath) + + // Then + XCTAssertEqual( + got, + .init() + ) + } + func test_loadWorkspace() throws { // Given let temporaryPath = try temporaryPath() @@ -175,7 +260,9 @@ final class ManifestLoaderTests: TuistTestCase { // Given let fileHandler = FileHandler() let temporaryPath = try temporaryPath() - try fileHandler.touch(temporaryPath.appending(component: "Config.swift")) + let configPath = temporaryPath.appending(component: "Config.swift") + try fileHandler.touch(configPath) + let data = try fileHandler.readFile(configPath) // When XCTAssertThrowsError( @@ -185,6 +272,7 @@ final class ManifestLoaderTests: TuistTestCase { error as? ManifestLoaderError, .manifestLoadingFailed( path: temporaryPath.appending(component: "Config.swift"), + data: data, context: """ The encoded data for the manifest is corrupted. The given data was not valid JSON. diff --git a/Tests/TuistLoaderTests/Loaders/ConfigLoaderTests.swift b/Tests/TuistLoaderTests/Loaders/ConfigLoaderTests.swift index 472ac6f96b7..6383a3f5bd1 100644 --- a/Tests/TuistLoaderTests/Loaders/ConfigLoaderTests.swift +++ b/Tests/TuistLoaderTests/Loaders/ConfigLoaderTests.swift @@ -100,8 +100,8 @@ final class ConfigLoaderTests: TuistUnitTestCase { "/project/Module/", "/project/Module/A/", ] - paths.forEach { - stub(path: $0, exists: true) + for item in paths { + stub(path: item, exists: true) } stub( config: .test(), diff --git a/Tests/TuistLoaderTests/Loaders/ManifestLoaderErrorTests.swift b/Tests/TuistLoaderTests/Loaders/ManifestLoaderErrorTests.swift index 52100fb1380..60106ebd408 100644 --- a/Tests/TuistLoaderTests/Loaders/ManifestLoaderErrorTests.swift +++ b/Tests/TuistLoaderTests/Loaders/ManifestLoaderErrorTests.swift @@ -25,8 +25,12 @@ final class ManifestLoaderErrorTests: TuistUnitTestCase { "Manifest not found at path /test" ) XCTAssertEqual( - ManifestLoaderError.manifestLoadingFailed(path: try AbsolutePath(validating: "/test/"), context: "Context") - .description, + ManifestLoaderError.manifestLoadingFailed( + path: try AbsolutePath(validating: "/test/"), + data: Data(), + context: "Context" + ) + .description, """ Unable to load manifest at \("/test".bold()) Context @@ -39,7 +43,11 @@ final class ManifestLoaderErrorTests: TuistUnitTestCase { XCTAssertEqual(ManifestLoaderError.unexpectedOutput(try AbsolutePath(validating: "/test/")).type, .bug) XCTAssertEqual(ManifestLoaderError.manifestNotFound(.project, try AbsolutePath(validating: "/test/")).type, .abort) XCTAssertEqual( - ManifestLoaderError.manifestLoadingFailed(path: try AbsolutePath(validating: "/test/"), context: "Context").type, + ManifestLoaderError.manifestLoadingFailed( + path: try AbsolutePath(validating: "/test/"), + data: Data(), + context: "Context" + ).type, .abort ) } diff --git a/Tests/TuistLoaderTests/Loaders/PackageSettingsLoaderTests.swift b/Tests/TuistLoaderTests/Loaders/PackageSettingsLoaderTests.swift new file mode 100644 index 00000000000..bb210855f1c --- /dev/null +++ b/Tests/TuistLoaderTests/Loaders/PackageSettingsLoaderTests.swift @@ -0,0 +1,69 @@ +import Foundation +import TSCBasic +import TuistCore +import TuistGraph +import TuistGraphTesting +import TuistSupport +import XCTest + +@testable import ProjectDescription +@testable import TuistLoader +@testable import TuistLoaderTesting +@testable import TuistSupportTesting + +final class PackageSettingsLoaderTests: TuistUnitTestCase { + private var manifestLoader: MockManifestLoader! + private var subject: PackageSettingsLoader! + + override func setUp() { + super.setUp() + + manifestLoader = MockManifestLoader() + subject = PackageSettingsLoader(manifestLoader: manifestLoader) + } + + override func tearDown() { + subject = nil + manifestLoader = nil + + super.tearDown() + } + + func test_loadDependencies() throws { + // Given + let temporaryPath = try temporaryPath() + let plugins = Plugins.test() + + manifestLoader.loadPackageSettingsStub = { _ in + PackageSettings( + platforms: [.iOS, .macOS] + ) + } + manifestLoader.loadDependenciesStub = { _ in + Dependencies( + carthage: [ + .github(path: "Dependency1", requirement: .exact("1.1.1")), + .git(path: "Dependency1", requirement: .exact("2.3.4")), + ], + swiftPackageManager: .init(), + platforms: [.iOS, .macOS] + ) + } + + // When + let got = try subject.loadPackageSettings(at: temporaryPath, with: plugins) + + // Then + let expected: TuistGraph.PackageSettings = .init( + productTypes: [:], + baseSettings: .init(configurations: [ + .debug: .init(settings: [:], xcconfig: nil), + .release: .init(settings: [:], xcconfig: nil), + ]), + targetSettings: [:], + platforms: [.iOS, .macOS] + ) + XCTAssertEqual(manifestLoader.registerPluginsCount, 1) + XCTAssertEqual(got, expected) + } +} diff --git a/Tests/TuistLoaderTests/Models+ManifestMappers/TargetScript+ManifestMapperTests.swift b/Tests/TuistLoaderTests/Models+ManifestMappers/TargetScript+ManifestMapperTests.swift index 0342583b19d..ec6612b506f 100644 --- a/Tests/TuistLoaderTests/Models+ManifestMappers/TargetScript+ManifestMapperTests.swift +++ b/Tests/TuistLoaderTests/Models+ManifestMappers/TargetScript+ManifestMapperTests.swift @@ -60,7 +60,7 @@ final class TargetScriptManifestMapperTests: TuistUnitTestCase { let model = try TuistGraph.TargetScript.from(manifest: manifest, generatorPaths: generatorPaths) // Then - let relativeSources = model.inputPaths.map { $0.relative(to: temporaryPath).pathString } + let relativeSources = try model.inputPaths.map { try AbsolutePath(validating: $0).relative(to: temporaryPath).pathString } XCTAssertEqual(Set(relativeSources), Set([ "${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Resources/DWARF/${TARGET_NAME}", @@ -82,7 +82,7 @@ final class TargetScriptManifestMapperTests: TuistUnitTestCase { ) XCTAssertEqual( model.outputPaths, - [temporaryPath.appending(try RelativePath(validating: "$(SRCROOT)/foo/bar/**/*.swift"))] + [temporaryPath.appending(try RelativePath(validating: "$(SRCROOT)/foo/bar/**/*.swift")).pathString] ) XCTAssertEqual( model.outputFileListPaths, @@ -126,7 +126,7 @@ final class TargetScriptManifestMapperTests: TuistUnitTestCase { let model = try TuistGraph.TargetScript.from(manifest: manifest, generatorPaths: generatorPaths) // Then - let relativeSources = model.inputPaths.map { $0.relative(to: temporaryPath).pathString } + let relativeSources = try model.inputPaths.map { try AbsolutePath(validating: $0).relative(to: temporaryPath).pathString } XCTAssertEqual(Set(relativeSources), Set([ "foo/bar/a.swift", @@ -143,7 +143,7 @@ final class TargetScriptManifestMapperTests: TuistUnitTestCase { ) XCTAssertEqual( model.outputPaths, - [temporaryPath.appending(try RelativePath(validating: "$(SRCROOT)/foo/bar/**/*.swift"))] + [temporaryPath.appending(try RelativePath(validating: "$(SRCROOT)/foo/bar/**/*.swift")).pathString] ) XCTAssertEqual( model.outputFileListPaths, diff --git a/Tests/TuistLoaderTests/Utils/ManifestFilesLocatorTests.swift b/Tests/TuistLoaderTests/Utils/ManifestFilesLocatorTests.swift index d1f107c4528..5fa30c902b8 100644 --- a/Tests/TuistLoaderTests/Utils/ManifestFilesLocatorTests.swift +++ b/Tests/TuistLoaderTests/Utils/ManifestFilesLocatorTests.swift @@ -485,6 +485,31 @@ final class ManifestFilesLocatorTests: TuistUnitTestCase { XCTAssertNil(configPath) } + func test_locatePackageManifest() throws { + // Given + let paths = try createFiles([ + "Module01/File01.swift", + "Module01/File02.swift", + "Module01/File03.swift", + + "Module02/File01.swift", + "Module02/File01.swift", + "Module02/Subdir01/File01.swift", + "Module02/Subdir01/File02.swift", + + "File01.swift", + "File02.swift", + "Tuist/Package.swift", + ]) + + // When + let packageManifestPath = subject.locatePackageManifest(at: try temporaryPath()) + + // Then + XCTAssertNotNil(packageManifestPath) + XCTAssertEqual(paths.last, packageManifestPath) + } + func test_locateDependencies() throws { // Given let paths = try createFiles([ diff --git a/Tests/TuistScaffoldTests/TemplateGeneratorTests.swift b/Tests/TuistScaffoldTests/TemplateGeneratorTests.swift index 817f5b76c32..ab381eb9334 100644 --- a/Tests/TuistScaffoldTests/TemplateGeneratorTests.swift +++ b/Tests/TuistScaffoldTests/TemplateGeneratorTests.swift @@ -111,8 +111,8 @@ final class TemplateGeneratorTests: TuistTestCase { ) // Then - try expectedFiles.forEach { - XCTAssertEqual(try FileHandler.shared.readTextFile($0.0), $0.1) + for expectedFile in expectedFiles { + XCTAssertEqual(try FileHandler.shared.readTextFile(expectedFile.0), expectedFile.1) } } @@ -159,8 +159,8 @@ final class TemplateGeneratorTests: TuistTestCase { ) // Then - try expectedFiles.forEach { - XCTAssertEqual(try FileHandler.shared.readTextFile($0.0), $0.1) + for expectedFile in expectedFiles { + XCTAssertEqual(try FileHandler.shared.readTextFile(expectedFile.0), expectedFile.1) } } @@ -197,8 +197,8 @@ final class TemplateGeneratorTests: TuistTestCase { ) // Then - try expectedFiles.forEach { - XCTAssertEqual(try FileHandler.shared.readTextFile($0.0), $0.1) + for expectedFile in expectedFiles { + XCTAssertEqual(try FileHandler.shared.readTextFile(expectedFile.0), expectedFile.1) } } diff --git a/Tests/TuistTestAcceptanceTests/TestAcceptanceTests.swift b/Tests/TuistTestAcceptanceTests/TestAcceptanceTests.swift index cfb4dcea316..5a6705206a4 100644 --- a/Tests/TuistTestAcceptanceTests/TestAcceptanceTests.swift +++ b/Tests/TuistTestAcceptanceTests/TestAcceptanceTests.swift @@ -6,14 +6,14 @@ import XCTest /// Test projects using tuist test final class TestAcceptanceTests: TuistAcceptanceTestCase { func test_with_app_with_framework_and_tests() async throws { - try setUpFixture("app_with_framework_and_tests") + try setUpFixture(.appWithFrameworkAndTests) try await run(TestCommand.self) try await run(TestCommand.self, "App") try await run(TestCommand.self, "--test-targets", "FrameworkTests/FrameworkTests") } func test_with_app_with_test_plan() async throws { - try setUpFixture("app_with_test_plan") + try setUpFixture(.appWithTestPlan) try await run(TestCommand.self) try await run(TestCommand.self, "App", "--test-plan", "All") } diff --git a/Tuist/Config.swift b/Tuist/Config.swift index f94b5a75f23..7afbc98fdc7 100644 --- a/Tuist/Config.swift +++ b/Tuist/Config.swift @@ -6,5 +6,6 @@ let config = Config( url: "https://cloud.tuist.io", options: [.optional] ), - swiftVersion: .init("5.8") + swiftVersion: .init("5.8"), + generationOptions: .options(enforceExplicitDependencies: true) ) diff --git a/Tuist/Package.resolved b/Tuist/Package.resolved index 4baaf8fca32..e8653e8167e 100644 --- a/Tuist/Package.resolved +++ b/Tuist/Package.resolved @@ -149,28 +149,10 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-argument-parser.git", "state" : { - "revision" : "8f4d2753f0e4778c76d5f05ad16c74f707390531", - "version" : "1.2.3" - } - }, - { - "identity" : "swift-docc-plugin", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-docc-plugin.git", - "state" : { - "revision" : "26ac5758409154cc448d7ab82389c520fa8a8247", + "revision" : "c8ed701b513cf5177118a175d85fbbbcd707ab41", "version" : "1.3.0" } }, - { - "identity" : "swift-docc-symbolkit", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-docc-symbolkit", - "state" : { - "revision" : "b45d1f2ed151d057b54504d653e0da5552844e34", - "version" : "1.0.0" - } - }, { "identity" : "swift-log", "kind" : "remoteSourceControl", @@ -201,7 +183,7 @@ { "identity" : "swifter", "kind" : "remoteSourceControl", - "location" : "https://github.com/httpswift/swifter", + "location" : "https://github.com/httpswift/swifter.git", "state" : { "revision" : "1e4f51c92d7ca486242d8bf0722b99de2c3531aa" } @@ -218,16 +200,16 @@ { "identity" : "xcbeautify", "kind" : "remoteSourceControl", - "location" : "https://github.com/tuist/xcbeautify", + "location" : "https://github.com/cpisciotta/xcbeautify", "state" : { - "revision" : "7dc31d4a9e9cc660f2de6ff6c6e0f0d5dfbb572b", - "version" : "1.0.1" + "revision" : "84d24a9854e6fdcd2c91122d50a3189b072e8136", + "version" : "1.4.0" } }, { "identity" : "xcodeproj", "kind" : "remoteSourceControl", - "location" : "https://github.com/tuist/XcodeProj.git", + "location" : "https://github.com/tuist/XcodeProj", "state" : { "revision" : "3797181813ee963fe305d939232bc576d23ddbb0", "version" : "8.15.0" diff --git a/Tuist/Package.swift b/Tuist/Package.swift index f5268411262..54e22a9c051 100644 --- a/Tuist/Package.swift +++ b/Tuist/Package.swift @@ -1,11 +1,10 @@ -// swift-tools-version: 5.8 +// swift-tools-version: 5.7 import PackageDescription let package = Package( name: "PackageName", dependencies: [ .package(url: "https://github.com/apple/swift-argument-parser", from: "1.2.3"), - .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.3.0"), .package(url: "https://github.com/apple/swift-log", from: "1.5.3"), .package(url: "https://github.com/apple/swift-tools-support-core", from: "0.6.1"), .package(url: "https://github.com/CombineCommunity/CombineExt", from: "1.8.1"), @@ -20,6 +19,6 @@ let package = Package( .package(url: "https://github.com/SwiftGen/StencilSwiftKit", from: "2.10.1"), .package(url: "https://github.com/SwiftGen/SwiftGen", from: "6.6.2"), .package(url: "https://github.com/tuist/XcodeProj", from: "8.15.0"), - .package(url: "https://github.com/tuist/xcbeautify", from: "1.0.1"), + .package(url: "https://github.com/cpisciotta/xcbeautify", from: "1.4.0"), ] ) diff --git a/Tuist/ProjectDescriptionHelpers/Constants.swift b/Tuist/ProjectDescriptionHelpers/Constants.swift deleted file mode 100644 index c68e0e6780f..00000000000 --- a/Tuist/ProjectDescriptionHelpers/Constants.swift +++ /dev/null @@ -1,5 +0,0 @@ -import ProjectDescription - -public enum Constants { - public static let deploymentTarget: DeploymentTarget = .macOS(targetVersion: "12.0") -} diff --git a/Tuist/ProjectDescriptionHelpers/Target+Helpers.swift b/Tuist/ProjectDescriptionHelpers/Target+Helpers.swift index e59728ec30d..50cf6ad1244 100644 --- a/Tuist/ProjectDescriptionHelpers/Target+Helpers.swift +++ b/Tuist/ProjectDescriptionHelpers/Target+Helpers.swift @@ -26,21 +26,16 @@ extension Target { } return Target( name: name, - platform: .macOS, + destinations: [.mac], product: product, bundleId: "io.tuist.\(name)", - deploymentTarget: Constants.deploymentTarget, + deploymentTargets: .macOS("12.0"), infoPlist: .default, sources: ["\(rootFolder)/\(name)/**/*.swift"], dependencies: dependencies ) } - /// - Parameters: - /// - dependencies: Dependencies for the main target. - /// - testDependencies: Dependencies for tests. - /// - testingDependencies: Dependencies for the testing target. - /// - integrationTestsDependencies: Dependencies for the integration tests. public static func module( name: String, product: Product = .staticFramework, diff --git a/assets/companies/crunchyroll_logo_vertical.svg b/assets/companies/crunchyroll_logo_vertical.svg new file mode 100644 index 00000000000..a5ba101f0b5 --- /dev/null +++ b/assets/companies/crunchyroll_logo_vertical.svg @@ -0,0 +1,5 @@ + + + diff --git a/docs/Package.resolved b/docs/Package.resolved new file mode 100644 index 00000000000..e65252d0740 --- /dev/null +++ b/docs/Package.resolved @@ -0,0 +1,23 @@ +{ + "pins" : [ + { + "identity" : "swift-docc-plugin", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-docc-plugin", + "state" : { + "revision" : "26ac5758409154cc448d7ab82389c520fa8a8247", + "version" : "1.3.0" + } + }, + { + "identity" : "swift-docc-symbolkit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-docc-symbolkit", + "state" : { + "revision" : "b45d1f2ed151d057b54504d653e0da5552844e34", + "version" : "1.0.0" + } + } + ], + "version" : 2 +} diff --git a/docs/Package.swift b/docs/Package.swift new file mode 100644 index 00000000000..fd6ef01fadf --- /dev/null +++ b/docs/Package.swift @@ -0,0 +1,21 @@ +// swift-tools-version:5.7 + +import PackageDescription + +let package = Package( + name: "tuist", + products: [ + .executable( + name: "tuist", + targets: ["tuist"] + ), + ], + dependencies: [ + .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.3.0"), + ], + targets: [ + .target( + name: "tuist" + ), + ] +) diff --git a/docs/Sources/tuist/ProjectDescription b/docs/Sources/tuist/ProjectDescription new file mode 120000 index 00000000000..3ace68863b9 --- /dev/null +++ b/docs/Sources/tuist/ProjectDescription @@ -0,0 +1 @@ +../../../Sources/ProjectDescription \ No newline at end of file diff --git a/docs/Sources/tuist/main.swift b/docs/Sources/tuist/main.swift new file mode 100644 index 00000000000..e69de29bb2d diff --git a/Sources/tuist/tuist.docc/Tutorials/Tuist Cloud Tutorial/Tuist Cloud Tutorial Enterprise/Tuist Cloud Tutorial - Enterprise Deployment.tutorial b/docs/Sources/tuist/tuist.docc/Articles/Tuist Cloud/Users/Tutorials/Tuist Cloud Tutorial/Tuist Cloud Tutorial Enterprise/enterprise-deployment.tutorial similarity index 100% rename from Sources/tuist/tuist.docc/Tutorials/Tuist Cloud Tutorial/Tuist Cloud Tutorial Enterprise/Tuist Cloud Tutorial - Enterprise Deployment.tutorial rename to docs/Sources/tuist/tuist.docc/Articles/Tuist Cloud/Users/Tutorials/Tuist Cloud Tutorial/Tuist Cloud Tutorial Enterprise/enterprise-deployment.tutorial diff --git a/Sources/tuist/tuist.docc/Tutorials/Tuist Cloud Tutorial/Tuist Cloud Tutorial Enterprise/Tuist Cloud Tutorial - Enterprise Environment.tutorial b/docs/Sources/tuist/tuist.docc/Articles/Tuist Cloud/Users/Tutorials/Tuist Cloud Tutorial/Tuist Cloud Tutorial Enterprise/enterprise-environment.tutorial similarity index 100% rename from Sources/tuist/tuist.docc/Tutorials/Tuist Cloud Tutorial/Tuist Cloud Tutorial Enterprise/Tuist Cloud Tutorial - Enterprise Environment.tutorial rename to docs/Sources/tuist/tuist.docc/Articles/Tuist Cloud/Users/Tutorials/Tuist Cloud Tutorial/Tuist Cloud Tutorial Enterprise/enterprise-environment.tutorial diff --git a/Sources/tuist/tuist.docc/Tutorials/Tuist Cloud Tutorial/Tuist Cloud Tutorial Enterprise/Tuist Cloud Tutorial - Enterprise Infrastructure Requirements.tutorial b/docs/Sources/tuist/tuist.docc/Articles/Tuist Cloud/Users/Tutorials/Tuist Cloud Tutorial/Tuist Cloud Tutorial Enterprise/enterprise-infrastructure-requirements.tutorial similarity index 100% rename from Sources/tuist/tuist.docc/Tutorials/Tuist Cloud Tutorial/Tuist Cloud Tutorial Enterprise/Tuist Cloud Tutorial - Enterprise Infrastructure Requirements.tutorial rename to docs/Sources/tuist/tuist.docc/Articles/Tuist Cloud/Users/Tutorials/Tuist Cloud Tutorial/Tuist Cloud Tutorial Enterprise/enterprise-infrastructure-requirements.tutorial diff --git a/Sources/tuist/tuist.docc/Tutorials/Tuist Cloud Tutorial/Tuist Cloud Tutorial.tutorial b/docs/Sources/tuist/tuist.docc/Articles/Tuist Cloud/Users/Tutorials/Tuist Cloud Tutorial/tuist-cloud-tutorials.tutorial similarity index 58% rename from Sources/tuist/tuist.docc/Tutorials/Tuist Cloud Tutorial/Tuist Cloud Tutorial.tutorial rename to docs/Sources/tuist/tuist.docc/Articles/Tuist Cloud/Users/Tutorials/Tuist Cloud Tutorial/tuist-cloud-tutorials.tutorial index 8b08a81b329..a375402f743 100644 --- a/Sources/tuist/tuist.docc/Tutorials/Tuist Cloud Tutorial/Tuist Cloud Tutorial.tutorial +++ b/docs/Sources/tuist/tuist.docc/Articles/Tuist Cloud/Users/Tutorials/Tuist Cloud Tutorial/tuist-cloud-tutorials.tutorial @@ -1,5 +1,5 @@ -@Tutorials(name: "Tuist Cloud Tutorial") { - @Intro(title: "Tutorials") { +@Tutorials(name: "Tuist Cloud") { + @Intro(title: "Tuist Cloud Tutorials") { Through these tutorials you will learn all about Tuist Cloud, from the basics like using the managed service to more advanced concepts like self-hosting a tuist cloud instance in your infrastructure. } @@ -8,9 +8,9 @@ @Image(source: "Tuist Cloud Enterprise Server.png", alt: "An illustration that shows the icon of a server") - @TutorialReference(tutorial: "doc:Tuist-Cloud-Tutorial---Enterprise-Infrastructure-Requirements") - @TutorialReference(tutorial: "doc:Tuist-Cloud-Tutorial---Enterprise-Environment") - @TutorialReference(tutorial: "doc:Tuist-Cloud-Tutorial---Enterprise-Deployment") + @TutorialReference(tutorial: "doc:enterprise-infrastructure-requirements") + @TutorialReference(tutorial: "doc:enterprise-environment") + @TutorialReference(tutorial: "doc:enterprise-deployment") } } diff --git a/Sources/tuist/tuist.docc/Articles/Tuist Cloud/Tuist Cloud - Binary caching.md b/docs/Sources/tuist/tuist.docc/Articles/Tuist Cloud/Users/binary-caching.md similarity index 93% rename from Sources/tuist/tuist.docc/Articles/Tuist Cloud/Tuist Cloud - Binary caching.md rename to docs/Sources/tuist/tuist.docc/Articles/Tuist Cloud/Users/binary-caching.md index e68a437ebd6..d3e8ad84baa 100644 --- a/Sources/tuist/tuist.docc/Articles/Tuist Cloud/Tuist Cloud - Binary caching.md +++ b/docs/Sources/tuist/tuist.docc/Articles/Tuist Cloud/Users/binary-caching.md @@ -30,6 +30,8 @@ tuist generate Search Settings # Dependencies, and Search and Settings dependenc tuist generate --no-cache # No cache at all ``` +> Warning: Binary caching is a feature designed for development workflows such as running the app on a simulator or device, or running tests. It is not intended for release builds. When archiving the app, generate a project with the sources by using the `--no-cache` flag. + ### Sharing binaries across environments To facilitate the sharing of binaries across different environments, you'll require a [Tuist Cloud](https://tuist.io/cloud) account and a designated project. You have the option to create this project directly under your personal account, or alternatively, you can establish an organization. Creating an organization allows you to invite your team members to collaborate within a unified framework: diff --git a/docs/Sources/tuist/tuist.docc/Articles/Tuist Cloud/Users/selective-testing.md b/docs/Sources/tuist/tuist.docc/Articles/Tuist Cloud/Users/selective-testing.md new file mode 100644 index 00000000000..fb70fafe7f4 --- /dev/null +++ b/docs/Sources/tuist/tuist.docc/Articles/Tuist Cloud/Users/selective-testing.md @@ -0,0 +1,34 @@ +# Selective Testing + +Run only tests that have changed since the last successful test run + +## Overview + +As your project grows, so does the amount of your tests. For a long time, running all tests on every PR or push to `main` takes tens of seconds. But this solution does not scale to thousands of tests your team might have. + +On every test run on the CI, you probably build a project with cleaned derived data and re-run all the tests, regardless of the changes. `tuist test` helps you to drastically decrease the build time and then running the tests themselves. + +### Using cache binaries + +When you call `tuist test`, tuist generates a project focusing on test targets only. It is similar to you running `tuist generate MyTestA MyTestB ...`, and so you automatically get the benefits of the [binary caching](./binary-caching). + +### Running tests selectively + +To run tests selectively, use the `tuist test` command. This command fingerprints your project the same way it does for [warming the cache](./binary-caching#Cache-warming). If the `tuist test` command succeeds, it will save those fingeprints in the Tuist cache. This effectively marks those as fingerprints as tested and from this point on, tuist will only re-run a test suite if a target's fingerpring the suite depends on is not present in the cache. + +For example, if you have test suites `FeatureATests`, `FeatureBTests` which depend on `FeatureA` and `FeatureB`, respectively, and both depend on a module `Core`, `tuist test` will behave as such: +```bash +tuist test # Initial test run, runs for both `FeatureATests` and `FeatureBTests` +# `FeatureA` module is updated +tuist test +# FeatureATests has not changed from last successful run, skipping.. +# Testing scheme Tuist-Workspace -> only FeatureBTests will be run + +# `Core` module is updated +tuist test +# Both FeatureATests and FeatureBTests will be run +``` + +The test results used for selective testing can be also shared across environments – this will be done automatically once your project is [set up with Tuist Cloud](./binary-caching#Sharing-binaries-across-environments). + +By reusing both binary caching and selective testing across environments, you will be able to dramatically reduce your test runs. diff --git a/Sources/tuist/tuist.docc/Articles/Tuist Cloud/Tuist Cloud - What.md b/docs/Sources/tuist/tuist.docc/Articles/Tuist Cloud/Users/tuist-cloud.md similarity index 88% rename from Sources/tuist/tuist.docc/Articles/Tuist Cloud/Tuist Cloud - What.md rename to docs/Sources/tuist/tuist.docc/Articles/Tuist Cloud/Users/tuist-cloud.md index 6791349c623..c11c7c1aecc 100644 --- a/Sources/tuist/tuist.docc/Articles/Tuist Cloud/Tuist Cloud - What.md +++ b/docs/Sources/tuist/tuist.docc/Articles/Tuist Cloud/Users/tuist-cloud.md @@ -16,13 +16,19 @@ Tuist Cloud, a closed-source paid service, enhances Tuist by adding server-requi ### Available -#### 📦 Binary caching across environments +#### Binary caching across environments Tuist Cloud offers a robust storage solution for Tuist, enabling the sharing of cache artifacts between local and remote settings, such as continuous integration. This ensures that developers avoid recompiling targets they don't intend to modify, provided they've already been compiled by a teammate or in a CI setting. Leveraging this caching can yield efficiency rates up to 90%, leading to significant time and cost savings for both local development and CI processes. > Tip: To assist organizations in evaluating their return on investment (ROI), we've developed an [**ROI calculator**](https://tuist.io/cloud). For instance, consider an organization with approximately 20 developers. If their clean builds take 10 minutes and they achieve a 70% cache effectiveness, they could potentially reduce development time by 24,000 hours and recover up to $6.4 million a year. -#### 📈 Basic insights +#### Selective testing across environments + +Once teams reach a certain scale, they often grapple with optimizing their CI process to maintain quick turnaround times. While **testing everything** continually might work for smaller teams, it becomes impractical on a larger scale. At this juncture, many teams resort to investing in superior hardware, creating custom tools, complicating their CI pipelines, or worse, accepting slower development cycles. But there's a better way. + +**Tuist Cloud utilizes graph knowledge and fingerprinting technology—essential for binary caching—to discern which targets to test based on file modifications.** Not only that, as your tests will also be able to use binary caching, massively reducing the time it takes to both _build_ and _run_ your tests. + +#### Insights While optimizing workflows based on our project insights is beneficial, it's crucial to ensure that your project's evolution doesn't lead to regressions, adversely affecting the developer experience. While our ultimate goal is to harness AI technologies to offer you a virtual co-pilot, we currently provide foundational insights to enhance your understanding of your project and workflows. This allows you to identify optimization opportunities and make data-driven decisions. We firmly believe this is data that Xcode ought to supply. However, recognizing the clear demand from teams, we're stepping up to deliver it. @@ -30,7 +36,7 @@ While optimizing workflows based on our project insights is beneficial, it's cru ### In development -#### 📈 Advanced actionable insights +#### Advanced actionable insights Regressions can easily compromise the health of a project, build, or test suites. This is primarily because CI workflows focus on ensuring successful compilation and test suite outcomes. As a result, developers tend to merge pull requests (PRs) once they're approved and both the compilation and test runs are successful. Yet, such PRs might inadvertently affect other vital aspects that directly influence developer productivity. For instance, they could: @@ -41,9 +47,3 @@ Regressions can easily compromise the health of a project, build, or test suites In a conventional setup, these issues often go unnoticed until they've become significant problems. By the time they're detected, teams face the daunting task of tracing back to find the root cause before implementing a fix. This becomes especially challenging in dynamic environments where changes are constantly integrated. However, there's a more efficient approach. We aim to **gather data from builds, including build times, binary sizes, and test outcomes, and integrate this with graph information.** This consolidated data will then be transmitted to our server. From there, developers can **visually track performance trends over time**. Our goal is to not only make this information easily accessible but also **actionable**. By identifying potential deviations that might hinder productivity, we can flag them directly in PRs. This proactive approach ensures that potential regressions are intercepted before merging into the primary repository branch. In essence, Tuist Cloud is designed to serve as a vigilant co-pilot, ensuring a **consistently healthy and efficient development environment**. An optimal development environment is pivotal for maintaining developers' enthusiasm and commitment to the project. - -#### ✅ Incremental build and test execution across environments - -Once teams reach a certain scale, they often grapple with optimizing their CI process to maintain quick turnaround times. While **building and testing everything** continually might work for smaller teams, it becomes impractical on a larger scale. At this juncture, many teams resort to investing in superior hardware, creating custom tools, complicating their CI pipelines, or worse, accepting slower development cycles. But there's a better way. - -**Tuist Cloud utilizes graph knowledge and fingerprinting technology—essential for binary caching—to discern which targets to build and test based on file modifications.** Best of all, this behavior can be fine-tuned using additional hashing keys. For instance, while you might choose to build and test everything on the `main` branch, you can adopt an incremental approach from the primary trunk when development diverges. This approach is not only efficient but also cost-effective for teams. diff --git a/Sources/tuist/tuist.docc/Articles/Contributing/Contributing - Championing Projects.md b/docs/Sources/tuist/tuist.docc/Articles/Tuist/Contributors/championing-projects.md similarity index 100% rename from Sources/tuist/tuist.docc/Articles/Contributing/Contributing - Championing Projects.md rename to docs/Sources/tuist/tuist.docc/Articles/Tuist/Contributors/championing-projects.md diff --git a/Sources/tuist/tuist.docc/Articles/Contributing/Contributing - Code Reviews.md b/docs/Sources/tuist/tuist.docc/Articles/Tuist/Contributors/code-reviews.md similarity index 92% rename from Sources/tuist/tuist.docc/Articles/Contributing/Contributing - Code Reviews.md rename to docs/Sources/tuist/tuist.docc/Articles/Tuist/Contributors/code-reviews.md index 4f5b24daf08..842e9b1866c 100644 --- a/Sources/tuist/tuist.docc/Articles/Contributing/Contributing - Code Reviews.md +++ b/docs/Sources/tuist/tuist.docc/Articles/Tuist/Contributors/code-reviews.md @@ -6,7 +6,7 @@ There are contribution traits that can't be automated: _design, code structure & This document puts together traits that we should look out for when reviewing pull requests: -## 👀 Readability +## Readability Does the code express its intention clearly? If you need to spend a bunch of time figuring out what the code does, the code implementation needs to be improved. @@ -14,26 +14,26 @@ Suggest splitting the code into smaller abstractions that are easier to understa Alternative, and as a last resource, they can add a comment explaining the reasoning behind it. Ask yourself if you'd be able to understand the code in a near future, without any surrounding context like the pull request description. -## 🌱 Small pull request +## Small pull request Large pull requests are hard to review and it's easier to miss out details. If a pull request becomes too large and unmanageable, suggest the author to break it down. -## 📦 Consistency +## Consistency It's important that the changes are consistent with the rest of the project. Inconsistencies complicate maintenance, and therefore we should avoid them. If there's an approach to output messages to the user, or report errors, we should stick to that. If the author disagrees with the project's standards, suggest them to open an issue where we can discuss them further. -## 🔬 Tests +## Tests Tests allow changing code with confidence. The code on pull requests should be tested, and all tests should pass. A good test is a test that consistently produces the same result and that it's easy to understand and maintain. Reviewers spend most of the review time in the implementation code, but tests are equally important because they are code too. -## ⚠️ Breaking changes +## Breaking changes Breaking changes are a bad user experience for users of Tuist. Contributions should avoid introducing breaking changes unless it's strictly necessary. @@ -42,21 +42,21 @@ Whether a change is breaking or not might not be obvious. A method to verify whether the change is breaking is running Tuist against the fixture projects in the `fixtures` directory. It requires putting ourselves in the user's shoes and imagine how the changes would impact them. -## 📝 Documentation +## Documentation As the project grows and we continue to add more features, keeping the documentation up to date is crucial for developers to adopt them. Moreover, it's a very valuable asset for new adopters that are giving Tuist a try. Pull requests that change the user interface of Tuist, for example adding support for a new argument, must include documentation. -## 📝 Changelog +## Changelog The PR contains one of the `changelog:*` label, and the title of the PR is an accurate description of the change, in the right form to become part of the Changelog. -## 🚦 Continuous integration: continuous integration must be happy with the changes. +## Continuous integration: continuous integration must be happy with the changes. The pipelines are designed to bring an extra level of confidence and validate that the changes are right. A red CI blocks the merge of the pull request. -## ✅ Merging +## Merging A pull request is ready to be merged once there are at least 2 approvals from [Tuist core members](https://github.com/orgs/tuist/people). diff --git a/Sources/tuist/tuist.docc/Articles/Contributing/Contributing - Get Started.md b/docs/Sources/tuist/tuist.docc/Articles/Tuist/Contributors/get-started-as-contributor.md similarity index 100% rename from Sources/tuist/tuist.docc/Articles/Contributing/Contributing - Get Started.md rename to docs/Sources/tuist/tuist.docc/Articles/Tuist/Contributors/get-started-as-contributor.md diff --git a/Sources/tuist/tuist.docc/Articles/Contributing/Contributing - Manifesto.md b/docs/Sources/tuist/tuist.docc/Articles/Tuist/Contributors/manifesto.md similarity index 100% rename from Sources/tuist/tuist.docc/Articles/Contributing/Contributing - Manifesto.md rename to docs/Sources/tuist/tuist.docc/Articles/Tuist/Contributors/manifesto.md diff --git a/Sources/tuist/tuist.docc/Articles/Contributing/Contributing - Reporting Bugs.md b/docs/Sources/tuist/tuist.docc/Articles/Tuist/Contributors/reporting-bugs.md similarity index 100% rename from Sources/tuist/tuist.docc/Articles/Contributing/Contributing - Reporting Bugs.md rename to docs/Sources/tuist/tuist.docc/Articles/Tuist/Contributors/reporting-bugs.md diff --git a/docs/Sources/tuist/tuist.docc/Articles/Tuist/Manifests/config.md b/docs/Sources/tuist/tuist.docc/Articles/Tuist/Manifests/config.md new file mode 100644 index 00000000000..c73c498cf2f --- /dev/null +++ b/docs/Sources/tuist/tuist.docc/Articles/Tuist/Manifests/config.md @@ -0,0 +1,45 @@ +# Config + +Use the Config manifest file to configure Tuist's functionalities globally. + +Tuist can be configured through a shared `Config.swift` manifest. +When Tuist is executed, it traverses up the directories to find a `Tuist` directory containing a `Config.swift` file. +Defining a configuration manifest is not required, but recommended to ensure a consistent behaviour across all the projects that are part of the repository. + +The example below shows a project that has a global `Config.swift` file that will be used when Tuist is run from any of the subdirectories: + +```bash +/Workspace.swift +/Tuist/Config.swift # Configuration manifest +/Framework/Project.swift +/App/Project.swift +``` + +That way, when executing Tuist in any of the subdirectories, it will use the shared configuration. + +The snippet below shows an example configuration manifest: + +```swift +import ProjectDescription + +let config = Config( + compatibleXcodeVersions: ["10.3"], + swiftVersion: "5.4.0", + generationOptions: .options( + xcodeProjectName: "SomePrefix-\(.projectName)-SomeSuffix", + organizationName: "Tuist", + developmentRegion: "de" + ) +) +``` + +## Topics + +### Related + +- ``Cache`` +- ``Cloud`` +- ``CompatibleXcodeVersions`` +- ``Plugin`` +- ``PluginLocation`` +- ``Version`` diff --git a/docs/Sources/tuist/tuist.docc/Articles/Tuist/Manifests/project.md b/docs/Sources/tuist/tuist.docc/Articles/Tuist/Manifests/project.md new file mode 100644 index 00000000000..c3cdc0df680 --- /dev/null +++ b/docs/Sources/tuist/tuist.docc/Articles/Tuist/Manifests/project.md @@ -0,0 +1,38 @@ +# Project + +Projects are defined in `Project.swift` files, which we refer to as manifest files. + +The snippet below shows an example project manifest: + +```swift +import ProjectDescription + +let project = Project( + name: "MyProject", + targets: [ + Target( + name: "App", + platform: .iOS, + product: .app, + bundleId: "io.tuist.App", + sources: ["Sources/**"] + ) + ] +) +``` + +## Topics + +### Configuring targets + +- ``Target`` + +### Configuring custom schemes + +- ``Scheme`` + +### Others + +- ``TestingOptions`` +- ``FileHeaderTemplate`` +- ``ResourceSynthesizer`` diff --git a/docs/Sources/tuist/tuist.docc/Articles/Tuist/Manifests/workspace.md b/docs/Sources/tuist/tuist.docc/Articles/Tuist/Manifests/workspace.md new file mode 100644 index 00000000000..711c05b11a7 --- /dev/null +++ b/docs/Sources/tuist/tuist.docc/Articles/Tuist/Manifests/workspace.md @@ -0,0 +1,21 @@ +# Workspace + +You can customize your generated workspace in the `Workspace.swift` file. + +By default, `tuist generate` generates an Xcode workspace that has the same name as the current project. It includes the project and all its dependencies. Tuist allows customizing this behaviour by defining a workspace manifest within a `Workspace.swift` file. Workspace manifests allow specifying a list of projects to generate and include in an Xcode workspace. Those projects don’t necessarily have to depend on one another. Additionally, files and folder references _(such as documentation files)_ can be included in a workspace manifest. + +The snippet below shows an example workspace manifest: + +```swift +import ProjectDescription + +let workspace = Workspace( + name: "CustomWorkspace", + projects: [ + "App", + "Modules/**" + ] +) +``` + +Although `Workspace.swift` file can reside in any directory (including a project directory), we recommend defining it at the root of the project. diff --git a/docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/Tutorials/Tuist Tutorial/Getting-Started/Add External Dependencies/Package.swift b/docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/Tutorials/Tuist Tutorial/Getting-Started/Add External Dependencies/Package.swift new file mode 100644 index 00000000000..a249e44f571 --- /dev/null +++ b/docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/Tutorials/Tuist Tutorial/Getting-Started/Add External Dependencies/Package.swift @@ -0,0 +1,21 @@ +// swift-tools-version: 5.8 +import PackageDescription + +#if TUIST + import ProjectDescription + import ProjectDescriptionHelpers + + let packageSettings = PackageSettings( + productTypes: [ + "Alamofire": .framework, // default is .staticFramework + ], + platforms: [.iOS] + ) +#endif + +let package = Package( + name: "PackageName", + dependencies: [ + .package(url: "https://github.com/Alamofire/Alamofire", from: "5.0.0"), + ] +) diff --git a/Sources/tuist/tuist.docc/Tutorials/Tuist Tutorial/Getting-Started/Add External Dependencies/Project.swift b/docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/Tutorials/Tuist Tutorial/Getting-Started/Add External Dependencies/Project.swift similarity index 100% rename from Sources/tuist/tuist.docc/Tutorials/Tuist Tutorial/Getting-Started/Add External Dependencies/Project.swift rename to docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/Tutorials/Tuist Tutorial/Getting-Started/Add External Dependencies/Project.swift diff --git a/docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/Tutorials/Tuist Tutorial/Getting-Started/Add External Dependencies/dependencies-generate.sh b/docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/Tutorials/Tuist Tutorial/Getting-Started/Add External Dependencies/dependencies-generate.sh new file mode 100644 index 00000000000..9cb332aa834 --- /dev/null +++ b/docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/Tutorials/Tuist Tutorial/Getting-Started/Add External Dependencies/dependencies-generate.sh @@ -0,0 +1,6 @@ +$ tuist generate + +Generating workspace App.xcworkspace +Generating project App +Project generated. +Total time taken: 0.242s diff --git a/docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/Tutorials/Tuist Tutorial/Getting-Started/Add External Dependencies/dependencies-gitignore.txt b/docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/Tutorials/Tuist Tutorial/Getting-Started/Add External Dependencies/dependencies-gitignore.txt new file mode 100644 index 00000000000..723178c34ce --- /dev/null +++ b/docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/Tutorials/Tuist Tutorial/Getting-Started/Add External Dependencies/dependencies-gitignore.txt @@ -0,0 +1,2 @@ +Tuist/Dependencies/graph.json +Tuist/Dependencies/SwiftPackageManager diff --git a/docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/Tutorials/Tuist Tutorial/Getting-Started/Add External Dependencies/external-dependencies.tutorial b/docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/Tutorials/Tuist Tutorial/Getting-Started/Add External Dependencies/external-dependencies.tutorial new file mode 100644 index 00000000000..3535fc039a2 --- /dev/null +++ b/docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/Tutorials/Tuist Tutorial/Getting-Started/Add External Dependencies/external-dependencies.tutorial @@ -0,0 +1,66 @@ +@Tutorial(time: 5) { + @Intro(title: "Adding external Dependencies") { + Learn how to integrate external Package dependencies into your Tuist project. + + @Image(source: "Logo-Blurred.png", alt: "Blurred Tuist Logo.") + } + + @Section(title: "Defining package dependencies") { + @ContentAndMedia { + External dependencies are defined in the `Tuist/Package.swift` manifest file. + } + + @Steps { + @Step { + Create a `Tuist/Package.swift` file defining your external dependencies and their versions with the same `Swift Package Manager` syntax you might be familiar with. + + You can also add additional Swift Package Manager configuration such as the desired target platform, or some custom mapping for your dependencies using the `PackageSettings`. The `packageSettings` object must be inside the `#if TUIST ... #endif` directive. + @Code(name: "Tuist/Package.swift", file: "Package.swift", reset: true) + + > Note: For example, here we are declaring that the `Alamofire` target should be mapped to a dynamic framework, instead of the default static framework. + } + } + } + + @Section(title: "Fetching package dependencies") { + @ContentAndMedia { + Unlike Xcode's default integration, which resolves the dependencies at launch time or whenever Xcode thinks resolution is necessary, we resolve them through the `tuist fetch` command. + } + + @Steps { + @Step { + Run `tuist fetch` to resolve the dependencies using the Swift Package Manager under `Tuist/Dependencies`. The generated workspace will contain projects and targets for them. + + @Code(name: "Console", file: "tuist-fetch.sh", reset: true) + } + + @Step { + Don't forget to ignore the following directories and files from your version control system. + @Code(name: ".gitignore", file: "dependencies-gitignore.txt", reset: true) + } + } + } + + @Section(title: "Integrating package dependencies") { + Once dependencies have been fetched, you can declare dependencies from your projects' targets. + + @Steps { + @Step { + Run `tuist edit` to edit your project's manifest. + @Code(name: "console", file: "tuist-edit.txt", reset: true) + } + + @Step { + Use the `.external` target dependency option to declare the dependency. + @Code(name: "Project.swift", file: "Project.swift", reset: true) + } + + @Step { + Generate the project, and try to build it. + @Code(name: "Project.swift", file: "Project.swift", reset: true) + } + } + } + +} + diff --git a/Sources/tuist/tuist.docc/Tutorials/Tuist Tutorial/Getting-Started/Add External Dependencies/tuist-edit.txt b/docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/Tutorials/Tuist Tutorial/Getting-Started/Add External Dependencies/tuist-edit.txt similarity index 100% rename from Sources/tuist/tuist.docc/Tutorials/Tuist Tutorial/Getting-Started/Add External Dependencies/tuist-edit.txt rename to docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/Tutorials/Tuist Tutorial/Getting-Started/Add External Dependencies/tuist-edit.txt diff --git a/docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/Tutorials/Tuist Tutorial/Getting-Started/Add External Dependencies/tuist-fetch.sh b/docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/Tutorials/Tuist Tutorial/Getting-Started/Add External Dependencies/tuist-fetch.sh new file mode 100644 index 00000000000..7e52e2a0148 --- /dev/null +++ b/docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/Tutorials/Tuist Tutorial/Getting-Started/Add External Dependencies/tuist-fetch.sh @@ -0,0 +1,5 @@ +$ tuist fetch + +Resolving and fetching dependencies. +Installing Swift Package Manager dependencies. +... diff --git a/Sources/tuist/tuist.docc/Tutorials/Tuist Tutorial/Getting-Started/Create Project/Tuist Tutorial - Create Project.tutorial b/docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/Tutorials/Tuist Tutorial/Getting-Started/Create Project/create-project.tutorial similarity index 100% rename from Sources/tuist/tuist.docc/Tutorials/Tuist Tutorial/Getting-Started/Create Project/Tuist Tutorial - Create Project.tutorial rename to docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/Tutorials/Tuist Tutorial/Getting-Started/Create Project/create-project.tutorial diff --git a/Sources/tuist/tuist.docc/Tutorials/Tuist Tutorial/Getting-Started/Create Project/inital-project-templates.swift b/docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/Tutorials/Tuist Tutorial/Getting-Started/Create Project/inital-project-templates.swift similarity index 100% rename from Sources/tuist/tuist.docc/Tutorials/Tuist Tutorial/Getting-Started/Create Project/inital-project-templates.swift rename to docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/Tutorials/Tuist Tutorial/Getting-Started/Create Project/inital-project-templates.swift diff --git a/Sources/tuist/tuist.docc/Tutorials/Tuist Tutorial/Getting-Started/Create Project/initial-project-file.swift b/docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/Tutorials/Tuist Tutorial/Getting-Started/Create Project/initial-project-file.swift similarity index 100% rename from Sources/tuist/tuist.docc/Tutorials/Tuist Tutorial/Getting-Started/Create Project/initial-project-file.swift rename to docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/Tutorials/Tuist Tutorial/Getting-Started/Create Project/initial-project-file.swift diff --git a/Sources/tuist/tuist.docc/Tutorials/Tuist Tutorial/Getting-Started/Create Project/initial-readme.txt b/docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/Tutorials/Tuist Tutorial/Getting-Started/Create Project/initial-readme.txt similarity index 100% rename from Sources/tuist/tuist.docc/Tutorials/Tuist Tutorial/Getting-Started/Create Project/initial-readme.txt rename to docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/Tutorials/Tuist Tutorial/Getting-Started/Create Project/initial-readme.txt diff --git a/Sources/tuist/tuist.docc/Tutorials/Tuist Tutorial/Getting-Started/Create Project/initial-run-output.txt b/docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/Tutorials/Tuist Tutorial/Getting-Started/Create Project/initial-run-output.txt similarity index 100% rename from Sources/tuist/tuist.docc/Tutorials/Tuist Tutorial/Getting-Started/Create Project/initial-run-output.txt rename to docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/Tutorials/Tuist Tutorial/Getting-Started/Create Project/initial-run-output.txt diff --git a/Sources/tuist/tuist.docc/Tutorials/Tuist Tutorial/Getting-Started/Create Project/make-directory.txt b/docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/Tutorials/Tuist Tutorial/Getting-Started/Create Project/make-directory.txt similarity index 100% rename from Sources/tuist/tuist.docc/Tutorials/Tuist Tutorial/Getting-Started/Create Project/make-directory.txt rename to docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/Tutorials/Tuist Tutorial/Getting-Started/Create Project/make-directory.txt diff --git a/Sources/tuist/tuist.docc/Tutorials/Tuist Tutorial/Getting-Started/Create Project/tuist-added-readme.txt b/docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/Tutorials/Tuist Tutorial/Getting-Started/Create Project/tuist-added-readme.txt similarity index 100% rename from Sources/tuist/tuist.docc/Tutorials/Tuist Tutorial/Getting-Started/Create Project/tuist-added-readme.txt rename to docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/Tutorials/Tuist Tutorial/Getting-Started/Create Project/tuist-added-readme.txt diff --git a/Sources/tuist/tuist.docc/Tutorials/Tuist Tutorial/Getting-Started/Create Project/tuist-generate.txt b/docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/Tutorials/Tuist Tutorial/Getting-Started/Create Project/tuist-generate.txt similarity index 100% rename from Sources/tuist/tuist.docc/Tutorials/Tuist Tutorial/Getting-Started/Create Project/tuist-generate.txt rename to docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/Tutorials/Tuist Tutorial/Getting-Started/Create Project/tuist-generate.txt diff --git a/Sources/tuist/tuist.docc/Tutorials/Tuist Tutorial/Getting-Started/Create Project/tuist-init-swiftui.txt b/docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/Tutorials/Tuist Tutorial/Getting-Started/Create Project/tuist-init-swiftui.txt similarity index 100% rename from Sources/tuist/tuist.docc/Tutorials/Tuist Tutorial/Getting-Started/Create Project/tuist-init-swiftui.txt rename to docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/Tutorials/Tuist Tutorial/Getting-Started/Create Project/tuist-init-swiftui.txt diff --git a/Sources/tuist/tuist.docc/Tutorials/Tuist Tutorial/Getting-Started/Create Project/tuist-init.txt b/docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/Tutorials/Tuist Tutorial/Getting-Started/Create Project/tuist-init.txt similarity index 100% rename from Sources/tuist/tuist.docc/Tutorials/Tuist Tutorial/Getting-Started/Create Project/tuist-init.txt rename to docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/Tutorials/Tuist Tutorial/Getting-Started/Create Project/tuist-init.txt diff --git a/docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/Tutorials/Tuist Tutorial/Getting-Started/Install Tuist/install-rtx.sh b/docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/Tutorials/Tuist Tutorial/Getting-Started/Install Tuist/install-rtx.sh new file mode 100644 index 00000000000..da63097c1d3 --- /dev/null +++ b/docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/Tutorials/Tuist Tutorial/Getting-Started/Install Tuist/install-rtx.sh @@ -0,0 +1,2 @@ +$ curl https://rtx.jdx.dev/rtx-latest-macos-arm64 > ~/bin/rtx +$ chmod +x ~/bin/rtx diff --git a/docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/Tutorials/Tuist Tutorial/Getting-Started/Install Tuist/install.tutorial b/docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/Tutorials/Tuist Tutorial/Getting-Started/Install Tuist/install.tutorial new file mode 100644 index 00000000000..45bc42bb5d2 --- /dev/null +++ b/docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/Tutorials/Tuist Tutorial/Getting-Started/Install Tuist/install.tutorial @@ -0,0 +1,41 @@ +@Tutorial(time: 2) { + @Intro(title: "Install Tuist") { + Learn how to install tuist and manage it. + + @Image(source: "Logo-Blurred.png", alt: "Blurred Tuist Logo.") + } + + @Section(title: "Install Tuist") { + @ContentAndMedia { + The first thing that we need to do to get started is install tuist. + } + + @Steps { + + @Step { + Install [rtx](https://github.com/jdx/rtx#quickstart) a tool to install, manage, and activate versions of Tuist in your environment. + @Code(name: "console", file: "install-rtx.sh") + } + + @Step { + Hook `rtx` into your shell to activate the right version of Tuist based on the current working directory. + @Code(name: "console", file: "rtx-shell.sh") + } + + @Step { + Install Tuist by running the following command + @Code(name: "console", file: "rtx-install-tuist.sh") + } + + @Step { + You can check if tuist has been installed correctly by getting its version. + @Code(name: "console", file: "tuist-version.sh") + } + + @Step { + You can also see the list of available commands, if you are curious. + @Code(name: "console", file: "tuist-help.sh") + } + } + } +} diff --git a/docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/Tutorials/Tuist Tutorial/Getting-Started/Install Tuist/rtx-install-tuist.sh b/docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/Tutorials/Tuist Tutorial/Getting-Started/Install Tuist/rtx-install-tuist.sh new file mode 100644 index 00000000000..fbe8311124e --- /dev/null +++ b/docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/Tutorials/Tuist Tutorial/Getting-Started/Install Tuist/rtx-install-tuist.sh @@ -0,0 +1,13 @@ +$ curl https://rtx.jdx.dev/rtx-latest-macos-arm64 > ~/bin/rtx +$ chmod +x ~/bin/rtx + +# For the bash shell +$ echo 'eval "$(~/bin/rtx activate bash)"' >> ~/.bashrc + +# For the zsh shell +$ echo 'eval "$(~/bin/rtx activate zsh)"' >> ~/.zshrc + +# For the Fish shell +$ echo '~/bin/rtx activate fish | source' >> ~/.config/fish/config.fish + +$ rtx install tuist diff --git a/docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/Tutorials/Tuist Tutorial/Getting-Started/Install Tuist/rtx-shell.sh b/docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/Tutorials/Tuist Tutorial/Getting-Started/Install Tuist/rtx-shell.sh new file mode 100644 index 00000000000..8d0a24b039b --- /dev/null +++ b/docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/Tutorials/Tuist Tutorial/Getting-Started/Install Tuist/rtx-shell.sh @@ -0,0 +1,11 @@ +$ curl https://rtx.jdx.dev/rtx-latest-macos-arm64 > ~/bin/rtx +$ chmod +x ~/bin/rtx + +# For the bash shell +$ echo 'eval "$(~/bin/rtx activate bash)"' >> ~/.bashrc + +# For the zsh shell +$ echo 'eval "$(~/bin/rtx activate zsh)"' >> ~/.zshrc + +# For the Fish shell +$ echo '~/bin/rtx activate fish | source' >> ~/.config/fish/config.fish diff --git a/Sources/tuist/tuist.docc/Tutorials/Tuist Tutorial/Getting-Started/Install Uninstall Tuist/tuist-help.txt b/docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/Tutorials/Tuist Tutorial/Getting-Started/Install Tuist/tuist-help.sh similarity index 82% rename from Sources/tuist/tuist.docc/Tutorials/Tuist Tutorial/Getting-Started/Install Uninstall Tuist/tuist-help.txt rename to docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/Tutorials/Tuist Tutorial/Getting-Started/Install Tuist/tuist-help.sh index 6d0c13d1a03..97adc5c14e6 100644 --- a/Sources/tuist/tuist.docc/Tutorials/Tuist Tutorial/Getting-Started/Install Uninstall Tuist/tuist-help.txt +++ b/docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/Tutorials/Tuist Tutorial/Getting-Started/Install Tuist/tuist-help.sh @@ -1,11 +1,23 @@ -% curl -Ls https://install.tuist.io | bash +$ curl https://rtx.jdx.dev/rtx-latest-macos-arm64 > ~/bin/rtx +$ chmod +x ~/bin/rtx -% tuist version +# For the bash shell +$ echo 'eval "$(~/bin/rtx activate bash)"' >> ~/.bashrc + +# For the zsh shell +$ echo 'eval "$(~/bin/rtx activate zsh)"' >> ~/.zshrc + +# For the Fish shell +$ echo '~/bin/rtx activate fish | source' >> ~/.config/fish/config.fish + +$ rtx install tuist + +$ tuist version 3.25.0 # You should get a semantic version like shown above. -% tuist +$ tuist OVERVIEW: Generate, build and test your Xcode projects. USAGE: tuist [--help-env] diff --git a/docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/Tutorials/Tuist Tutorial/Getting-Started/Install Tuist/tuist-version.sh b/docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/Tutorials/Tuist Tutorial/Getting-Started/Install Tuist/tuist-version.sh new file mode 100644 index 00000000000..352f2fd2912 --- /dev/null +++ b/docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/Tutorials/Tuist Tutorial/Getting-Started/Install Tuist/tuist-version.sh @@ -0,0 +1,18 @@ +$ curl https://rtx.jdx.dev/rtx-latest-macos-arm64 > ~/bin/rtx +$ chmod +x ~/bin/rtx + +# For the bash shell +$ echo 'eval "$(~/bin/rtx activate bash)"' >> ~/.bashrc + +# For the zsh shell +$ echo 'eval "$(~/bin/rtx activate zsh)"' >> ~/.zshrc + +# For the Fish shell +$ echo '~/bin/rtx activate fish | source' >> ~/.config/fish/config.fish + +$ rtx install tuist + +$ tuist version +3.25.0 + +# You should get a semantic version like shown above. diff --git a/Sources/tuist/tuist.docc/Tutorials/Tuist Tutorial/Tuist Tutorial.tutorial b/docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/Tutorials/Tuist Tutorial/tuist-tutorials.tutorial similarity index 71% rename from Sources/tuist/tuist.docc/Tutorials/Tuist Tutorial/Tuist Tutorial.tutorial rename to docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/Tutorials/Tuist Tutorial/tuist-tutorials.tutorial index afb63778a33..6b85fd21349 100644 --- a/Sources/tuist/tuist.docc/Tutorials/Tuist Tutorial/Tuist Tutorial.tutorial +++ b/docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/Tutorials/Tuist Tutorial/tuist-tutorials.tutorial @@ -8,8 +8,8 @@ @Image(source: "Logo.png", alt: "An illustration that shows the Tuist logo.") - @TutorialReference(tutorial: "doc:Tuist-Tutorial---Install-Uninstall-Tuist") - @TutorialReference(tutorial: "doc:Tuist-Tutorial---Create-Project") - @TutorialReference(tutorial: "doc:Tuist-Tutorial---Add-External-Dependencies") + @TutorialReference(tutorial: "doc:install") + @TutorialReference(tutorial: "doc:create-project") + @TutorialReference(tutorial: "doc:external-dependencies") } } diff --git a/docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/command-line-interface.md b/docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/command-line-interface.md new file mode 100644 index 00000000000..2a05209b1d8 --- /dev/null +++ b/docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/command-line-interface.md @@ -0,0 +1,81 @@ +# Command line interface (CLI) + +This page contains an overview of the command line interface (CLI) of Tuist. + +## Overview + +`tuist` is the command line interface (CLI) that you use to interact with Tuist. It's a binary that you can install () in your system and use to generate, build, test, and more your Xcode projects. In the following sections, you'll learn about the different commands that you can use to interact with Tuist. +If you want to learn more about the commands, you can run `tuist help` to get a list of all the available commands and `tuist help ` to get more information about a specific command. + +### Init + +`tuist init` is the command that you use to initialise a new project from a template. + +### Generate + +`tuist generate` is the command that you use to generate an Xcode workspace from a project manifest. It's the most common command that you'll use when working with Tuist. + +### Edit + +`tuist edit` is the command that you use to edit manifests using Xcode. It generates an Xcode project with the manifest files and opens it in Xcode. The lifecycle of the Xcode project is tied to the lifecycle of the command execution. Once the command is aborted, the Xcode project is deleted. When you're done editing the manifest, you can run `tuist generate` to generate the Xcode project. + +**We recommend editing the manifest files using Xcode** because it provides a better experience than editing them using a text editor. For instance, Xcode provides syntax highlighting, code completion, and more. + + +### Fetch + +`tuist fetch` is the command that you use to fetch the Package dependencies () in the `Package.swift` file. It resolves the dependencies using the Swift Package Manager and generates the `Package.resolved` file. When your Xcode projects are generated, Tuist generates Xcode projects for the dependencies and links them to the Xcode projects of your project. + +### Build + +`tuist build` is the command that you use to build your project. It generates the Xcode project and builds it using the `xcodebuild` command line tool. It builds all the buildable targets in the project or the schemes that you pass as arguments to the command. + +> Tip: Although you can generate the project and build it with `xcodebuild` or any automation tool that wraps it, we recommend using `tuist build` because it can leverage the graph information to build the project faster (). + +### Test + +`tuist test` is the command that you use to test your project. It generates the Xcode project and tests it using the `xcodebuild` command line tool. It tests all the testable targets in the project or the schemes that you pass as arguments to the command. + +> Tip: `tuist test` leverages the graph information to speed up the test execution by skipping compilation steps () and selectively running the tests impacted by the changes. + +### Run + +`tuist run` is the command that you use to run your project. It generates the Xcode project and runs it using the `xcodebuild` command line tool. It runs the runnable scheme passed as an argument to the command. + +> Important: Only iOS apps are currently supported. We plan to support other product types and platforms. + +### Cache + +`tuist cache` contains a set of commands that you can use to interact with the cache. The most common command is `tuist cache warm` which warms up the cache with binary artifacts that Tuist uses to generate more optimized Xcode projects and provide faster builds and tests (). + +### Dump + +`tuist dump` outputs a manifest as a JSON. It's useful when you want to inspect through a serialised representation of the manifest. + +### Scaffold + +`tuist scaffold` is the command that you use to scaffold new files in your project from templates (). It's useful when you want to add new files to your project and you don't want to do it manually. + +### Migration + +`tuist migration` contains a set of commands that you can use to migrate your Xcode project () to a Tuist. For example, you can use `tuist migration settings-to-xcconfig` to extract build settings into `.xcconfig` files or `tuist migration check-empty-settings` to check a target or project for empty build settings. + +### Graph + +`tuist graph` is the command that you use to visualise the dependency graph of your project. You can output it in different formats such as `.dot`, `.png`, or `.svg` and use it to understand the dependencies between the targets in your project. + +### Clean + +`tuist clean` contains a set of commands that you can use to clean artifacts stored globally in your system. For example, `tuist clean manifests` cleans the cache that Tuist users to speed up the compilation of the manifest files. + +### Signing + +`tuist signing` contains a set of commands that allows you to keep certificates and provisioning profiles encrypted in your repository (). Tuist can decrypt them, install certificates in the Keychain, and configure the generated Xcode projects accordingly. + +### Cloud + +`tuist cloud` contains a set of commands that you can use to interact with the Tuist Cloud service (). You can authenticate and manage organizations and projects. + +### Plugin + +`tuist plugin` contains a set of commands to build, run, test, and archive plugins, which are Swift packages that extend Tuist's functionality (). \ No newline at end of file diff --git a/docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/dependencies.md b/docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/dependencies.md new file mode 100644 index 00000000000..dce1a9a2260 --- /dev/null +++ b/docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/dependencies.md @@ -0,0 +1,119 @@ +# Dependencies + +Learn how to declare internal and external dependencies in your project. + +## Overview + +When a project grows, it's common to split it into multiple targets to share code, define boundaries, and improve build times. +Multiple targets means defining dependencies between them forming a **dependency graph**, which might include external dependencies as well. + +Due to Xcode and XcodeProj's design, +the maintenance of a dependency graph can be a tedious and error-prone task. +Here are some examples of the problems that you might encounter: + +- Because Xcode's build system outputs all the project's products into the same directory in derived data, targets might be able to import products that they shouldn't. Compilations might fail on CI, where clean builds are more common, or later on when a different configuration is used. +- The transitive dynamic dependencies of a target need to be copied into any of the directories that are part of the `LD_RUNPATH_SEARCH_PATHS` build setting. If they aren't, the target won't be able to find them at runtime. This is easy to think about and set up when the graph is small, but it becomes a problem as the graph grows. +- When a target links a static [XCFramework](https://developer.apple.com/documentation/xcode/creating-a-multi-platform-binary-framework-bundle), the target needs an additional build phase for Xcode to process the bundle and extract the right binary for the current platform and architecture. This build phase is not added automatically, and it's easy to forget to add it. + +The above are just a few examples, but there are many more that we've encountered over the years. +Imagine if you required a team of engineers to maintain a dependency graph and ensure its validity. +Or even worse, +that the intricacies were resolved at build-time by a closed-source build system that you can't control or customize. +Sounds familiar? This is the approach that Apple took with Xcode and XcodeProj and that the Swift Package Manager has inherited. + +We strongly believe that the dependency graph should be **explicit** and **static** because only then can it be **validated** and **optimized**. +With Tuist, you focus on describing what depends on what, and we take care of the rest. +The intricacies and implementation details are abstracted away from you. + +In the following sections you'll learn how to declare dependencies in your project. + +> Note: Tuist validates the graph when generating the project to ensure that there are no cycles and that all the dependencies are valid. Thanks to this, any team can take part in evolving the dependency graph without worrying about breaking it. + +## Internal dependencies + +Targets can depend on other targets in the same and different projects, and on binaries. +When instantiating a `Target`, you can pass the `dependencies` argument with any of the following options: + +- `Target`: Declares a dependency with a target within the same project. +- `Project`: Declares a dependency with a target in a different project. +- `Framework`: Declares a dependency with a binary framework. +- `Library`: Declares a dependency with a binary library. +- `XCFramework`: Declares a dependency with a binary XCFramework. +- `SDK`: Declares a dependency with a system SDK. +- `XCTest`: Declares a dependency with XCTest. + +Note that every dependency type accepts a `condition` option to conditionally link the dependency based on the platform. By default, it links the dependency for all platforms the target supports. + +> Warning: We haven't yet solved the problem of targets being able to import dependencies that they shouldn't. Some users have implemented their custom solutions to detect this, but we haven't yet found a solution that we're happy with. We are currently exploring customizing the directory where products are outputted to solve this problem. + +## External dependencies + +Tuist also allows you to declare external dependencies in your project. + +### Swift Packages + +Swift Packages are our recommended way of declaring dependencies in your project. +You can integrate them using Xcode's default integration mechanism or using Tuist's XcodeProj-based integration. + +#### Tuist's XcodeProj-based integration + +Xcode's default integration while being the most convenient one, +lacks flexibility and control that's required for medium and large projects. +To overcome this, Tuist offers an XcodeProj-based integration that allows you to integrate Swift Packages in your project using XcodeProj's targets. +Thanks to that, we can not only give you more control over the integration but also make it compatible with workflows like binary caching () and selective testing (). + +XcodeProj's integration is more likely to take more time to support new Swift Package features or handle more package configurations. However, the mapping logic between Swift Packages and XcodeProj targets is open-source and can be contributed to by the community. This is contrary to Xcode's default integration, which is closed-source and maintained by Apple. + +You can follow the tutorial to learn more about how to integrate Swift Packages in your project. + +> Note: The **schemes** are not automatically created for Swift Package projects to keep the schemes list clean. You can create them via Xcode's UI. + +> Note: The **product type** defaults to static framework when none is specified. + +> Note: The **module maps** are not automatically generated for packages containing Objective-C code. You can override the `HEADER_SEARCH_PATHS` build setting to point to the headers directory ([details](https://github.com/tuist/tuist/issues/4180)). + +#### Xcode's default integration + +If you want to use Xcode's default integration mechanism, you can pass the list `packages` when instantiating a project: + +```swift +let project = Project(name: "MyProject", packages: [ + .remote(url: "https://github.com/krzyzanowskim/CryptoSwift", requirement: .exact("1.8.0")) +]) +``` + +And then reference them from your targets: + +```swift +let target = Target(name: "MyTarget", dependencies: [ + .package(product: "CryptoSwift", type: .runtime) +]) +``` + +For Swift Macros and Build Tool Plugins, you'll need to use the types `.macro` and `.plugin` respectively. + +### Carthage + +Since [Carthage](https://github.com/carthage/carthage) outputs `frameworks` or `xcframeworks`, you can run `carthage update` to output the dependencies in the `Carthage/Build` directory and then use the `.framework` or `.xcframework` target dependency type to declare the dependency in your target. You can wrap this in a script that you can run before generating the project. + +```bash +#!/usr/bin/env bash + +carthage update +tuist generate +``` + +> Warning: Carthage dependencies are not compatible with workflows like `build` or `test` that run `xcodebuild` right after generating the project. We have plans to add support for pre and post generation hooks. + +### CocoaPods + +[CocoaPods](https://cocoapods.org) expects an Xcode project to integrate the dependencies. You can use Tuist to generate the project, and then run `pod install` to integrate the dependencies by creating a workspace that contains your project and the Pods dependencies. You can wrap this in a script that you can run before generating the project. + +```bash +#!/usr/bin/env bash + +tuist generate +pod install +``` + +> Warning: CocoaPods dependencies are not compatible with workflows like `build` or `test` that run `xcodebuild` right after generating the project. They are also incompatible with binary caching and selective testing since the fingerprinting logic doesn't account for the Pods dependencies. diff --git a/docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/extensions.md b/docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/extensions.md new file mode 100644 index 00000000000..3bced922a7e --- /dev/null +++ b/docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/extensions.md @@ -0,0 +1,11 @@ +# Extensions + +Learn about how you can extend Tuist's functionality. + +## Overview + + + +### Section header + + diff --git a/docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/installation.md b/docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/installation.md new file mode 100644 index 00000000000..4620b363403 --- /dev/null +++ b/docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/installation.md @@ -0,0 +1,158 @@ +# Installation + +Learn how to install Tuist in your environment + +## Overview + +Tuist is a [command-line application](https://en.wikipedia.org/wiki/Command-line_interface) that you need to install in your environment before you can use it. The installation consists of an executable, dynamic frameworks, and a set of resources (for example, templates). Although you could manually build Tuist from the sources, we recommend using one of the following installation methods to ensure a valid installation. + +### Recommended: [mise](https://github.com/jdx/mise) + +Tuist defaults to [mise](https://github.com/jdx/mise) as a tool to deterministically manage and activate versions of Tuist. +If you don't have it installed on your system, +you can use any of these [installation methods](https://mise.jdx.dev/getting-started.html). +Remember to add the suggested line to your shell, which will ensure the right version is activated when you choose a Tuist project directory in your terminal session. + +> Note: mise is recommended over alternatives like [Homebrew](https://brew.sh) because it supports scoping and activating versions to directories, ensuring every environment uses the same version of Tuist deterministically. + +Once installed, you can install Tuist through any of the following commands: + + +```bash +# Tuist +mise install tuist # Install the current version specified in .tool-versions/.mise.toml +mise install tuist@x.y.z # Install a specific version number +mise install tuist@3 # Install a fuzzy version number +mise use tuist@x.y.z # Use tuist-x.y.z in the current project +mise use -g tuist@x.y.z # Use tuist-x.y.z as the global default +mise use tuist@latest # Use the latest tuist in the current directory +mise use -g tuist@system # Use the system's tuist as the global default + +# Tuist Cloud (For users of Tuist Cloud) +# Note: You need to install one OR the other, not both +mise install tuist-cloud # Install the current version specified in .tool-versions/.mise.toml +mise install tuist-cloud@x.y.z +mise install tuist-cloud@3 +mise use tuist-cloud@x.y.z +mise use -g tuist-cloud@x.y.z # Use tuist-cloud-x.y.z as the global default +mise use tuist-cloud@latest # Use the latest tuist-cloud in the current directory +mise use -g tuist-cloud@system # Use the system's tuist-cloud as the global default +``` + +#### Continuous integration + +If you are using Tuist in a continuous integration environment, the following sections show how to install Tuist in the most common ones: + +##### Xcode Cloud + +You'll need a [post-clone](https://developer.apple.com/documentation/xcode/writing-custom-build-scripts#Create-a-custom-build-script) script that installs Mise and Tuist: + +```bash +#!/bin/sh +curl https://mise.jdx.dev/install.sh | sh +mise install # Installs the version from .mise.toml + +# Runs the version of Tuist indicated in the .mise.toml file +mise x tuist generate +``` + +##### Codemagic + +To install and use Mise and Tuist in [Codemagic](https://codemagic.io), you can add an additional step to your workflow, and run Tuist through `mise x tuist` to ensure that the right version is used: + +```yaml +workflows: + lint: + name: Build + max_build_duration: 30 + environment: + xcode: 15.0.1 + scripts: + - name: Install Mise + script: | + curl https://mise.jdx.dev/install.sh | sh + mise install # Installs the version from .mise.toml + - name: Build + script: mise x tuist build +``` + +##### GitHub Actions + +On GitHub Actions you can use the [mise-action](https://github.com/jdx/mise-action), which abstracts the installation of Mise and Tuist and the configuration of the environment: + +```yaml +name: test +on: + pull_request: + branches: + - main + push: + branches: + - main +jobs: + lint: + runs-on: macos-latest + steps: + - uses: actions/checkout@v3 + - uses: jdx/mise-action@v2 +``` + + +> Tip: We recommend using `mise use --pin` in your Tuist projects to pin the version of Tuist across environments. The command will create a `.tool-versions` file containing the version of Tuist. + +> Important: **Tuist Cloud** (), a closed-source extension of Tuist with optimizations such as binary caching and selective testing, is distributed as a different asdf plugin, tuist-cloud. Note that by using it, you agree to the [Tuist Cloud Terms of Service](https://tuist.io/terms/). + +### Alternative: Homebrew + +If version pinning across environments is not a concern for you, +you can install Tuist using [Homebrew](https://brew.sh) and [our formulas](https://github.com/tuist/homebrew-tuist): + +```bash +brew tap tuist/tuist +brew install tuist +brew install tuist@x.y.z +brew install tuist-cloud # If you are a Tuist Cloud user +``` + +### Shell completions + + + +If you have Tuist **globally installed**, +you can install shell completions for Bash and Zsh to autocomplete commands and options. + +#### Zsh + +If you have [oh-my-zsh](https://ohmyz.sh/) installed, you already have a directory of automatically loading completion scripts — `.oh-my-zsh/completions`. Copy your new completion script to a new file in that directory called `_tuist`: + +```bash +tuist --generate-completion-script > ~/.oh-my-zsh/completions/_tuist +``` + +Without `oh-my-zsh`, you'll need to add a path for completion scripts to your function path, and turn on completion script autoloading. First, add these lines to `~/.zshrc`: + +```bash +fpath=(~/.zsh/completion $fpath) +autoload -U compinit +compinit +``` + +Next, create a directory at `~/.zsh/completion` and copy the completion script to the new directory, again into a file called `_tuist`. + +```bash +tuist --generate-completion-script > ~/.zsh/completion/_tuist +``` + +#### Bash + +If you have [bash-completion](https://github.com/scop/bash-completion) installed, you can just copy your new completion script to file `/usr/local/etc/bash_completion.d/_tuist`: + +```bash +tuist --generate-completion-script > /usr/local/etc/bash_completion.d/_tuist +``` + +Without bash-completion, you'll need to source the completion script directly. Copy it to a directory such as `~/.bash_completions/`, and then add the following line to `~/.bash_profile` or `~/.bashrc`: + +```bash +source ~/.bash_completions/example.bash +``` \ No newline at end of file diff --git a/docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/migration-guidelines.md b/docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/migration-guidelines.md new file mode 100644 index 00000000000..6c9cae852b1 --- /dev/null +++ b/docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/migration-guidelines.md @@ -0,0 +1,11 @@ +# Migration guidelines + +These guidelines help you migrate your Xcode project to Tuist. + +## Overview + +Text + +### Section header + +Text diff --git a/docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/project-structure.md b/docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/project-structure.md new file mode 100644 index 00000000000..cee74942091 --- /dev/null +++ b/docs/Sources/tuist/tuist.docc/Articles/Tuist/Users/project-structure.md @@ -0,0 +1,41 @@ +# Project structure + +Learn about the structure of Tuist projects and how to organize them. + +## Overview + +Although Tuist projects are commonly used to supersede Xcode projects, they are not limited to this use case. Tuist projects are also used to generate other types of projects, such as templates, plugins, and tasks. This document describes the structure of Tuist projects and how to organize them. + +### Xcode projects + +Xcode projects are **the most common type of project generated by Tuist.** They are used to build apps, frameworks, and libraries among others. Unlike Xcode projects, Tuist projects are defined in Swift, which makes them more flexible and easier to maintain. Tuist projects are also more declarative, which makes them easier to understand and reason about. The following structure shows a typical Tuist project that generates an Xcode project: + +> Note: The definition files are referred to as **manifest files** throughout the documentation. + +```bash +Tuist/ + Config.swift + Package.swift + ProjectDescriptionHelpers/ +Projects/ + App/ + Project.swift + Feature/ + Project.swift +Workspace.swift +``` + +- **Tuist directory:** This directory has two purposes. First, it signals **where the root of the project is**. This allows constructing paths relative to the root of the project, and also also running Tuist commands from any directory within the project. Second, it's the container for the following files: + - **Config.swift:** This file contains configuration for Tuist that's shared across all the projects, workspaces, and environments. For example, it can be used to disable automatic generation of schemes, or to define the deployment target of the projects. + - **Package.swift:** This file contains Swift Package dependencies for Tuist to integrate them using Xcode projects and targets (like [CocoaPods](https://cococapods)) that are configurable and optimizable. + - **ProjectDescriptionHelpers:** This directory contains Swift code that's shared across all the manifest files. Manifest files can `import ProjectDescriptionHelpers` to use the code defined in this directory. Sharing code is useful to avoid duplications and ensure consistency across the projects. + +- **Workspace.swift:** This manifest represents an Xcode workspace. It's used to group other projects and can also add additional files and schemes. +- **Project.swift:** This manifest represents an Xcode project. It's used to define the targets that are part of the project, and their dependencies. + +When interacting with the above project, commads expect to find either a `Workspace.swift` or a `Project.swift` file in the working directory or the directory indicated via the `--path` flag. The manifest should be in a directory or subdirectory of a directory containing a `Tuist` directory, which represents the root of the project. + +> Tip: Xcode workspaces allowed splitting projects into multiple Xcode projects to reduce the likelihood of merge conflicts. If that's what you were using workspaces for, you don't need them in Tuist. Tuist auto-generates a workspace containing a project and its dependencies' projects. + + + + + + + + + + + + + + + + + + diff --git a/fixtures/ios_app_with_implicit_dependencies/Targets/App/Sources/AppDelegate.swift b/fixtures/ios_app_with_implicit_dependencies/Targets/App/Sources/AppDelegate.swift new file mode 100644 index 00000000000..2b1895697e3 --- /dev/null +++ b/fixtures/ios_app_with_implicit_dependencies/Targets/App/Sources/AppDelegate.swift @@ -0,0 +1,24 @@ +import FrameworkA +import FrameworkB +import UIKit + +@main +class AppDelegate: UIResponder, UIApplicationDelegate { + var window: UIWindow? + + func application( + _: UIApplication, + didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]? = nil + ) -> Bool { + window = UIWindow(frame: UIScreen.main.bounds) + let viewController = UIViewController() + viewController.view.backgroundColor = .white + window?.rootViewController = viewController + window?.makeKeyAndVisible() + + FrameworkA.frameworkA() + FrameworkB.frameworkB() + + return true + } +} diff --git a/fixtures/ios_app_with_implicit_dependencies/Targets/App/Tests/AppTests.swift b/fixtures/ios_app_with_implicit_dependencies/Targets/App/Tests/AppTests.swift new file mode 100644 index 00000000000..70c453ba3f3 --- /dev/null +++ b/fixtures/ios_app_with_implicit_dependencies/Targets/App/Tests/AppTests.swift @@ -0,0 +1,8 @@ +import Foundation +import XCTest + +final class AppTests: XCTestCase { + func test_twoPlusTwo_isFour() { + XCTAssertEqual(2 + 2, 4) + } +} diff --git a/fixtures/ios_app_with_implicit_dependencies/Targets/FrameworkA/Sources/FrameworkA.swift b/fixtures/ios_app_with_implicit_dependencies/Targets/FrameworkA/Sources/FrameworkA.swift new file mode 100644 index 00000000000..8968896cfac --- /dev/null +++ b/fixtures/ios_app_with_implicit_dependencies/Targets/FrameworkA/Sources/FrameworkA.swift @@ -0,0 +1,9 @@ +import Foundation +import FrameworkB + +public enum FrameworkA { + public static func frameworkA() { + FrameworkB.frameworkB() + print("frameworkB") + } +} diff --git a/fixtures/ios_app_with_implicit_dependencies/Targets/FrameworkB/Sources/FrameworkB.swift b/fixtures/ios_app_with_implicit_dependencies/Targets/FrameworkB/Sources/FrameworkB.swift new file mode 100644 index 00000000000..b04ec17f7aa --- /dev/null +++ b/fixtures/ios_app_with_implicit_dependencies/Targets/FrameworkB/Sources/FrameworkB.swift @@ -0,0 +1,7 @@ +import Foundation + +public enum FrameworkB { + public static func frameworkB() { + print("frameworkB") + } +} diff --git a/fixtures/ios_app_with_implicit_dependencies/Targets/FrameworkC/Sources/FrameworkC.swift b/fixtures/ios_app_with_implicit_dependencies/Targets/FrameworkC/Sources/FrameworkC.swift new file mode 100644 index 00000000000..a5b772560c4 --- /dev/null +++ b/fixtures/ios_app_with_implicit_dependencies/Targets/FrameworkC/Sources/FrameworkC.swift @@ -0,0 +1,5 @@ +public struct FrameworkC { + public func frameworkC() { + print("frameworkC") + } +} diff --git a/fixtures/ios_app_with_implicit_dependencies/Tuist/Config.swift b/fixtures/ios_app_with_implicit_dependencies/Tuist/Config.swift new file mode 100644 index 00000000000..03ec9221cc4 --- /dev/null +++ b/fixtures/ios_app_with_implicit_dependencies/Tuist/Config.swift @@ -0,0 +1,7 @@ +import ProjectDescription + +let config = Config( + generationOptions: .options( + enforceExplicitDependencies: true + ) +) diff --git a/fixtures/ios_app_with_static_frameworks/Modules/C/Sources/C.swift b/fixtures/ios_app_with_static_frameworks/Modules/C/Sources/C.swift index 23743e728df..7f6f257dfd0 100755 --- a/fixtures/ios_app_with_static_frameworks/Modules/C/Sources/C.swift +++ b/fixtures/ios_app_with_static_frameworks/Modules/C/Sources/C.swift @@ -1,5 +1,6 @@ import D import Foundation + public enum C { public static let value: String = "cValue" diff --git a/fixtures/ios_app_with_static_frameworks_with_resources/Modules/C/Sources/C.swift b/fixtures/ios_app_with_static_frameworks_with_resources/Modules/C/Sources/C.swift index 23743e728df..7f6f257dfd0 100755 --- a/fixtures/ios_app_with_static_frameworks_with_resources/Modules/C/Sources/C.swift +++ b/fixtures/ios_app_with_static_frameworks_with_resources/Modules/C/Sources/C.swift @@ -1,5 +1,6 @@ import D import Foundation + public enum C { public static let value: String = "cValue" diff --git a/fixtures/ios_workspace_with_microfeature_architecture/Frameworks/FeatureContracts/Sources/FeatureBContract.swift b/fixtures/ios_workspace_with_microfeature_architecture/Frameworks/FeatureContracts/Sources/FeatureBContract.swift index 1939d78f9eb..f41a35613f4 100644 --- a/fixtures/ios_workspace_with_microfeature_architecture/Frameworks/FeatureContracts/Sources/FeatureBContract.swift +++ b/fixtures/ios_workspace_with_microfeature_architecture/Frameworks/FeatureContracts/Sources/FeatureBContract.swift @@ -1,5 +1,6 @@ import Core import Foundation + public protocol FeatureBContract { func run() func expose() -> CoreClass diff --git a/fixtures/ios_workspace_with_microfeature_architecture/Plugin/Plugin.swift b/fixtures/ios_workspace_with_microfeature_architecture/Plugin/Plugin.swift index 6f566adadc4..5cdff74f341 100644 --- a/fixtures/ios_workspace_with_microfeature_architecture/Plugin/Plugin.swift +++ b/fixtures/ios_workspace_with_microfeature_architecture/Plugin/Plugin.swift @@ -1,2 +1,3 @@ import ProjectDescription + let plugin = Plugin(name: "BundlePlugin") diff --git a/fixtures/ios_workspace_with_microfeature_architecture/Plugin/ProjectDescriptionHelpers/String+BundleId.swift b/fixtures/ios_workspace_with_microfeature_architecture/Plugin/ProjectDescriptionHelpers/String+BundleId.swift index 63f8e5f02e2..c44618c05a1 100644 --- a/fixtures/ios_workspace_with_microfeature_architecture/Plugin/ProjectDescriptionHelpers/String+BundleId.swift +++ b/fixtures/ios_workspace_with_microfeature_architecture/Plugin/ProjectDescriptionHelpers/String+BundleId.swift @@ -1,4 +1,5 @@ import ProjectDescription + extension String { /// Returns a canonical bundle Id for the target with the /// given name diff --git a/fixtures/ios_workspace_with_microfeature_architecture_static_linking/Frameworks/FeatureContracts/Sources/FeatureBContract.swift b/fixtures/ios_workspace_with_microfeature_architecture_static_linking/Frameworks/FeatureContracts/Sources/FeatureBContract.swift index 1939d78f9eb..f41a35613f4 100644 --- a/fixtures/ios_workspace_with_microfeature_architecture_static_linking/Frameworks/FeatureContracts/Sources/FeatureBContract.swift +++ b/fixtures/ios_workspace_with_microfeature_architecture_static_linking/Frameworks/FeatureContracts/Sources/FeatureBContract.swift @@ -1,5 +1,6 @@ import Core import Foundation + public protocol FeatureBContract { func run() func expose() -> CoreClass diff --git a/fixtures/multiplatform_app_with_extension/.gitignore b/fixtures/multiplatform_app_with_extension/.gitignore new file mode 100644 index 00000000000..551a3f50447 --- /dev/null +++ b/fixtures/multiplatform_app_with_extension/.gitignore @@ -0,0 +1,70 @@ +### macOS ### +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### Xcode ### +# Xcode +# +# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore + +## User settings +xcuserdata/ + +## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) +*.xcscmblueprint +*.xccheckout + +## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) +build/ +DerivedData/ +*.moved-aside +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 + +### Xcode Patch ### +*.xcodeproj/* +!*.xcodeproj/project.pbxproj +!*.xcodeproj/xcshareddata/ +!*.xcworkspace/contents.xcworkspacedata +/*.gcno + +### Projects ### +*.xcodeproj +*.xcworkspace + +### Tuist derived files ### +graph.dot +Derived/ + +### Tuist managed dependencies ### +Tuist/Dependencies diff --git a/fixtures/multiplatform_app_with_extension/App/Sources/TuistApp.swift b/fixtures/multiplatform_app_with_extension/App/Sources/TuistApp.swift new file mode 100644 index 00000000000..7989b1df37a --- /dev/null +++ b/fixtures/multiplatform_app_with_extension/App/Sources/TuistApp.swift @@ -0,0 +1,10 @@ +import SwiftUI + +@main +struct TuistApp: App { + var body: some Scene { + WindowGroup { + Text("Tuist is great") + } + } +} diff --git a/fixtures/multiplatform_app_with_extension/Extensions/WidgetExtension/Resources/Assets.xcassets/AccentColor.colorset/Contents.json b/fixtures/multiplatform_app_with_extension/Extensions/WidgetExtension/Resources/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 00000000000..eb878970081 --- /dev/null +++ b/fixtures/multiplatform_app_with_extension/Extensions/WidgetExtension/Resources/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/fixtures/multiplatform_app_with_extension/Extensions/WidgetExtension/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json b/fixtures/multiplatform_app_with_extension/Extensions/WidgetExtension/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000000..9221b9bb1a3 --- /dev/null +++ b/fixtures/multiplatform_app_with_extension/Extensions/WidgetExtension/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,98 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "83.5x83.5" + }, + { + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/fixtures/multiplatform_app_with_extension/Extensions/WidgetExtension/Resources/Assets.xcassets/Contents.json b/fixtures/multiplatform_app_with_extension/Extensions/WidgetExtension/Resources/Assets.xcassets/Contents.json new file mode 100644 index 00000000000..73c00596a7f --- /dev/null +++ b/fixtures/multiplatform_app_with_extension/Extensions/WidgetExtension/Resources/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/fixtures/multiplatform_app_with_extension/Extensions/WidgetExtension/Resources/Assets.xcassets/WidgetBackground.colorset/Contents.json b/fixtures/multiplatform_app_with_extension/Extensions/WidgetExtension/Resources/Assets.xcassets/WidgetBackground.colorset/Contents.json new file mode 100644 index 00000000000..eb878970081 --- /dev/null +++ b/fixtures/multiplatform_app_with_extension/Extensions/WidgetExtension/Resources/Assets.xcassets/WidgetBackground.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/fixtures/multiplatform_app_with_extension/Extensions/WidgetExtension/Sources/Widget.swift b/fixtures/multiplatform_app_with_extension/Extensions/WidgetExtension/Sources/Widget.swift new file mode 100644 index 00000000000..eac88f57911 --- /dev/null +++ b/fixtures/multiplatform_app_with_extension/Extensions/WidgetExtension/Sources/Widget.swift @@ -0,0 +1,69 @@ +#if canImport(WidgetKit) + + import SwiftUI + import WidgetKit + + struct Provider: TimelineProvider { + public typealias Entry = SimpleEntry + + func placeholder(in _: Context) -> SimpleEntry { + SimpleEntry(date: Date()) + } + + func getSnapshot(in _: Context, completion _: @escaping (SimpleEntry) -> Void) {} + + func getTimeline(in _: Context, completion _: @escaping (Timeline) -> Void) {} + + public func snapshot(with _: Context, completion: @escaping (SimpleEntry) -> Void) { + let entry = SimpleEntry(date: Date()) + completion(entry) + } + + public func timeline(with _: Context, completion: @escaping (Timeline) -> Void) { + var entries: [SimpleEntry] = [] + + // Generate a timeline consisting of five entries an hour apart, starting from the current date. + let currentDate = Date() + for hourOffset in 0 ..< 5 { + let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)! + let entry = SimpleEntry(date: entryDate) + entries.append(entry) + } + + let timeline = Timeline(entries: entries, policy: .atEnd) + completion(timeline) + } + } + + struct SimpleEntry: TimelineEntry { + public let date: Date + } + + struct PlaceholderView: View { + var body: some View { + Text("Placeholder View") + } + } + + struct MyWidgetEntryView: View { + var entry: Provider.Entry + + var body: some View { + Text(entry.date, style: .time) + } + } + + @main + struct MyWidget: Widget { + private let kind: String = "MyWidget" + + public var body: some WidgetConfiguration { + StaticConfiguration(kind: kind, provider: Provider()) { entry in + MyWidgetEntryView(entry: entry) + } + .configurationDisplayName("MyWidget") + .description("This is an example widget.") + } + } + +#endif diff --git a/fixtures/multiplatform_app_with_extension/Project.swift b/fixtures/multiplatform_app_with_extension/Project.swift new file mode 100644 index 00000000000..ca9a7dbaba5 --- /dev/null +++ b/fixtures/multiplatform_app_with_extension/Project.swift @@ -0,0 +1,57 @@ +import ProjectDescription + +let appTarget = Target( + name: "App", + destinations: [.iPhone, .iPad, .appleVision], + product: .app, + bundleId: "io.tuist.App", + infoPlist: "Support/App-Info.plist", + sources: "App/Sources/**", + dependencies: [ + .target(name: "WidgetExtension", condition: .when([.ios])), + .target(name: "WatchApp", condition: .when([.ios])), + ] +) + +let widgetExtensionTarget = Target( + name: "WidgetExtension", + destinations: [.iPhone, .iPad], + product: .appExtension, + bundleId: "io.tuist.App.WidgetExtension", + infoPlist: .extendingDefault(with: [ + "CFBundleDisplayName": "$(PRODUCT_NAME)", + "NSExtension": [ + "NSExtensionPointIdentifier": "com.apple.widgetkit-extension", + ], + ]), + sources: "Extensions/WidgetExtension/Sources/**", + resources: "Extensions/WidgetExtension/Resources/**" +) + +let watchApp = Target( + name: "WatchApp", + destinations: [.appleWatch], + product: .app, + bundleId: "io.tuist.App.watchkitapp", + infoPlist: nil, + sources: "WatchApp/Sources/**", + resources: "WatchApp/Resources/**", + settings: .settings( + base: [ + "GENERATE_INFOPLIST_FILE": true, + "CURRENT_PROJECT_VERSION": "1.0", + "MARKETING_VERSION": "1.0", + "INFOPLIST_KEY_UISupportedInterfaceOrientations": [ + "UIInterfaceOrientationPortrait", + "UIInterfaceOrientationPortraitUpsideDown", + ], + "INFOPLIST_KEY_WKCompanionAppBundleIdentifier": "io.tuist.App", + "INFOPLIST_KEY_WKRunsIndependentlyOfCompanionApp": false, + ] + ) +) + +let project = Project( + name: "App", + targets: [appTarget, widgetExtensionTarget, watchApp] +) diff --git a/fixtures/multiplatform_app_with_extension/Support/App-Info.plist b/fixtures/multiplatform_app_with_extension/Support/App-Info.plist new file mode 100644 index 00000000000..c407fafb087 --- /dev/null +++ b/fixtures/multiplatform_app_with_extension/Support/App-Info.plist @@ -0,0 +1,43 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSRequiresIPhoneOS + + NSHumanReadableCopyright + Copyright ©. All rights reserved. + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/fixtures/multiplatform_app_with_extension/Tuist/Config.swift b/fixtures/multiplatform_app_with_extension/Tuist/Config.swift new file mode 100644 index 00000000000..ff8e64fa9ee --- /dev/null +++ b/fixtures/multiplatform_app_with_extension/Tuist/Config.swift @@ -0,0 +1,3 @@ +import ProjectDescription + +let config = Config() diff --git a/fixtures/multiplatform_app_with_extension/WatchApp/Resources/Assets.xcassets/AccentColor.colorset/Contents.json b/fixtures/multiplatform_app_with_extension/WatchApp/Resources/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 00000000000..eb878970081 --- /dev/null +++ b/fixtures/multiplatform_app_with_extension/WatchApp/Resources/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/fixtures/multiplatform_app_with_extension/WatchApp/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json b/fixtures/multiplatform_app_with_extension/WatchApp/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000000..49c81cd8c4c --- /dev/null +++ b/fixtures/multiplatform_app_with_extension/WatchApp/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,13 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "watchos", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/fixtures/multiplatform_app_with_extension/WatchApp/Resources/Assets.xcassets/Contents.json b/fixtures/multiplatform_app_with_extension/WatchApp/Resources/Assets.xcassets/Contents.json new file mode 100644 index 00000000000..73c00596a7f --- /dev/null +++ b/fixtures/multiplatform_app_with_extension/WatchApp/Resources/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/fixtures/multiplatform_app_with_extension/WatchApp/Sources/WatchApp.swift b/fixtures/multiplatform_app_with_extension/WatchApp/Sources/WatchApp.swift new file mode 100644 index 00000000000..6bb8790cb20 --- /dev/null +++ b/fixtures/multiplatform_app_with_extension/WatchApp/Sources/WatchApp.swift @@ -0,0 +1,10 @@ +import SwiftUI + +@main +struct WatchApp: App { + var body: some Scene { + WindowGroup { + Text("Tuist is great") + } + } +} diff --git a/features/resources/WorkflowExtensionsSDK.pkg b/fixtures/resources/WorkflowExtensionsSDK.pkg similarity index 100% rename from features/resources/WorkflowExtensionsSDK.pkg rename to fixtures/resources/WorkflowExtensionsSDK.pkg diff --git a/fixtures/tuist_plugin/Package.resolved b/fixtures/tuist_plugin/Package.resolved index 6cc22ffd6c3..d5ce1f30a17 100644 --- a/fixtures/tuist_plugin/Package.resolved +++ b/fixtures/tuist_plugin/Package.resolved @@ -2,239 +2,14 @@ "object": { "pins": [ { - "package": "AEXML", - "repositoryURL": "https://github.com/tadija/AEXML.git", + "package": "ProjectAutomation", + "repositoryURL": "https://github.com/tuist/ProjectAutomation", "state": { - "branch": null, - "revision": "38f7d00b23ecd891e1ee656fa6aeebd6ba04ecc3", - "version": "4.6.1" - } - }, - { - "package": "Signals", - "repositoryURL": "https://github.com/tuist/BlueSignals.git", - "state": { - "branch": null, - "revision": "1f6c49e186c8a4eeef87ba14f2f97b8646559d13", - "version": "1.0.200" - } - }, - { - "package": "Checksum", - "repositoryURL": "https://github.com/rnine/Checksum.git", - "state": { - "branch": null, - "revision": "cd1ae53384dd578a84a0afef492a4f5d6202b068", - "version": "1.0.2" - } - }, - { - "package": "CombineExt", - "repositoryURL": "https://github.com/CombineCommunity/CombineExt.git", - "state": { - "branch": null, - "revision": "0880829102152185190064fd17847a7c681d2127", - "version": "1.5.1" - } - }, - { - "package": "Commandant", - "repositoryURL": "https://github.com/Carthage/Commandant.git", - "state": { - "branch": null, - "revision": "ab68611013dec67413628ac87c1f29e8427bc8e4", - "version": "0.17.0" - } - }, - { - "package": "Commander", - "repositoryURL": "https://github.com/kylef/Commander.git", - "state": { - "branch": null, - "revision": "4a1f2fb82fb6cef613c4a25d2e38f702e4d812c2", - "version": "0.9.2" - } - }, - { - "package": "CryptoSwift", - "repositoryURL": "https://github.com/krzyzanowskim/CryptoSwift.git", - "state": { - "branch": null, - "revision": "4b0565384d3c4c588af09e660535b2c7c9bf5b39", - "version": "1.4.2" - } - }, - { - "package": "GraphViz", - "repositoryURL": "https://github.com/tuist/GraphViz.git", - "state": { - "branch": "tuist", - "revision": "6a5f65e2208d66dc2547eb9d2c09213c98b6928d", + "branch": "main", + "revision": "f5cee58d487526bb604ff8a36a22a1922a65e2b5", "version": null } }, - { - "package": "Kanna", - "repositoryURL": "https://github.com/tid-kijyun/Kanna.git", - "state": { - "branch": null, - "revision": "f9e4922223dd0d3dfbf02ca70812cf5531fc0593", - "version": "5.2.7" - } - }, - { - "package": "KeychainAccess", - "repositoryURL": "https://github.com/kishikawakatsumi/KeychainAccess.git", - "state": { - "branch": null, - "revision": "84e546727d66f1adc5439debad16270d0fdd04e7", - "version": "4.2.2" - } - }, - { - "package": "Komondor", - "repositoryURL": "https://github.com/shibapm/Komondor.git", - "state": { - "branch": null, - "revision": "3fd348252fd14b5524fbaca9a42fdcfbe678657b", - "version": "1.1.2" - } - }, - { - "package": "Logger", - "repositoryURL": "https://github.com/shibapm/Logger", - "state": { - "branch": null, - "revision": "53c3ecca5abe8cf46697e33901ee774236d94cce", - "version": "0.2.3" - } - }, - { - "package": "Nimble", - "repositoryURL": "https://github.com/Quick/Nimble.git", - "state": { - "branch": null, - "revision": "7a46a5fc86cb917f69e3daf79fcb045283d8f008", - "version": "8.1.2" - } - }, - { - "package": "PackageConfig", - "repositoryURL": "https://github.com/shibapm/PackageConfig.git", - "state": { - "branch": null, - "revision": "5df1709f87e40e27b3fbe5022b826aff30d652d9", - "version": "1.1.0" - } - }, - { - "package": "PathKit", - "repositoryURL": "https://github.com/kylef/PathKit.git", - "state": { - "branch": null, - "revision": "3bfd2737b700b9a36565a8c94f4ad2b050a5e574", - "version": "1.0.1" - } - }, - { - "package": "Queuer", - "repositoryURL": "https://github.com/FabrizioBrancati/Queuer.git", - "state": { - "branch": null, - "revision": "52515108d0ac4616d9e15ffcc7ad986e300d31ff", - "version": "2.1.1" - } - }, - { - "package": "Quick", - "repositoryURL": "https://github.com/Quick/Quick.git", - "state": { - "branch": null, - "revision": "09b3becb37cb2163919a3842a4c5fa6ec7130792", - "version": "2.2.1" - } - }, - { - "package": "Rocket", - "repositoryURL": "https://github.com/f-meloni/Rocket", - "state": { - "branch": null, - "revision": "9880a5beb7fcb9e61ddd5764edc1700b8c418deb", - "version": "1.2.1" - } - }, - { - "package": "RxSwift", - "repositoryURL": "https://github.com/ReactiveX/RxSwift.git", - "state": { - "branch": null, - "revision": "cec68169a048a079f461ba203fe85636548d7a89", - "version": "5.1.3" - } - }, - { - "package": "ShellOut", - "repositoryURL": "https://github.com/JohnSundell/ShellOut.git", - "state": { - "branch": null, - "revision": "e1577acf2b6e90086d01a6d5e2b8efdaae033568", - "version": "2.3.0" - } - }, - { - "package": "SourceKitten", - "repositoryURL": "https://github.com/jpsim/SourceKitten.git", - "state": { - "branch": null, - "revision": "c0f960f72fa1e6151695074ffa696e4da6c45ce8", - "version": "0.30.1" - } - }, - { - "package": "Spectre", - "repositoryURL": "https://github.com/kylef/Spectre.git", - "state": { - "branch": null, - "revision": "26cc5e9ae0947092c7139ef7ba612e34646086c7", - "version": "0.10.1" - } - }, - { - "package": "Stencil", - "repositoryURL": "https://github.com/stencilproject/Stencil.git", - "state": { - "branch": null, - "revision": "ccd9402682f4c07dac9561befd207c8156e80e20", - "version": "0.14.2" - } - }, - { - "package": "StencilSwiftKit", - "repositoryURL": "https://github.com/SwiftGen/StencilSwiftKit.git", - "state": { - "branch": null, - "revision": "54cbedcdbb4334e03930adcff7343ffaf317bf0f", - "version": "2.8.0" - } - }, - { - "package": "swift-argument-parser", - "repositoryURL": "https://github.com/apple/swift-argument-parser.git", - "state": { - "branch": null, - "revision": "6b2aa2748a7881eebb9f84fb10c01293e15b52ca", - "version": "0.5.0" - } - }, - { - "package": "swift-log", - "repositoryURL": "https://github.com/apple/swift-log.git", - "state": { - "branch": null, - "revision": "5d66f7ba25daf4f94100e7022febf3c75e37a6c7", - "version": "1.4.2" - } - }, { "package": "swift-tools-support-core", "repositoryURL": "https://github.com/apple/swift-tools-support-core.git", @@ -243,96 +18,6 @@ "revision": "f9bbd6b80d67408021576adf6247e17c2e957d92", "version": "0.2.4" } - }, - { - "package": "Swifter", - "repositoryURL": "https://github.com/fortmarek/swifter.git", - "state": { - "branch": "stable", - "revision": "2e99c49c8575e6d18eb606c6142402f004cead79", - "version": null - } - }, - { - "package": "SwiftFormat", - "repositoryURL": "https://github.com/nicklockwood/SwiftFormat.git", - "state": { - "branch": null, - "revision": "c6ef3d0700b32f6512feb31a4a9fbc8122433af9", - "version": "0.49.0" - } - }, - { - "package": "SwiftGen", - "repositoryURL": "https://github.com/SwiftGen/SwiftGen", - "state": { - "branch": null, - "revision": "3b26e254b095d44f3dad06110bcb948b318898d6", - "version": "6.5.0" - } - }, - { - "package": "SwiftLint", - "repositoryURL": "https://github.com/Realm/SwiftLint.git", - "state": { - "branch": null, - "revision": "d53fc2664df92ef322bfa9ce5238d34f1461526a", - "version": "0.42.0" - } - }, - { - "package": "SwiftShell", - "repositoryURL": "https://github.com/kareman/SwiftShell", - "state": { - "branch": null, - "revision": "a6014fe94c3dbff0ad500e8da4f251a5d336530b", - "version": "5.1.0-beta.1" - } - }, - { - "package": "SwiftyTextTable", - "repositoryURL": "https://github.com/scottrhoyt/SwiftyTextTable.git", - "state": { - "branch": null, - "revision": "c6df6cf533d120716bff38f8ff9885e1ce2a4ac3", - "version": "0.9.0" - } - }, - { - "package": "SWXMLHash", - "repositoryURL": "https://github.com/drmohundro/SWXMLHash.git", - "state": { - "branch": null, - "revision": "9183170d20857753d4f331b0ca63f73c60764bf3", - "version": "5.0.2" - } - }, - { - "package": "tuist", - "repositoryURL": "https://github.com/tuist/tuist", - "state": { - "branch": "cc762b1d4", - "revision": "cc762b1d4fba6b3c9e0e41783496f9d8ea8f918e", - "version": null - } - }, - { - "package": "XcodeProj", - "repositoryURL": "https://github.com/tuist/XcodeProj.git", - "state": { - "branch": null, - "revision": "aa2a42c7a744ca18b5918771fdd6cf40f9753db5", - "version": "8.6.0" - } - }, - { - "package": "Zip", - "repositoryURL": "https://github.com/maparoni/Zip.git", - "state": { - "branch": null, - "revision": "059e7346082d02de16220cd79df7db18ddeba8c3", - "version": null - } } ] }, diff --git a/fixtures/tuist_plugin/Package.swift b/fixtures/tuist_plugin/Package.swift index ca1d3a4dd6d..3c90b8f9589 100644 --- a/fixtures/tuist_plugin/Package.swift +++ b/fixtures/tuist_plugin/Package.swift @@ -5,7 +5,7 @@ import PackageDescription let package = Package( name: "LocalPlugin", - platforms: [.macOS(.v10_15)], + platforms: [.macOS(.v11)], products: [ .executable( name: "tuist-create-file", @@ -18,20 +18,20 @@ let package = Package( ], dependencies: [ // Dependencies declare other packages that this package depends on. - .package(url: "https://github.com/tuist/tuist", .revision("cc762b1d4")), + .package(url: "https://github.com/tuist/ProjectAutomation", .branch("main")), ], targets: [ .target( name: "CreateFile", dependencies: [ - .product(name: "ProjectAutomation", package: "tuist") + .product(name: "ProjectAutomation", package: "ProjectAutomation") ] ), .testTarget(name: "CreateFileTests"), .target( name: "InspectGraph", dependencies: [ - .product(name: "ProjectAutomation", package: "tuist") + .product(name: "ProjectAutomation", package: "ProjectAutomation") ] ), ] diff --git a/make/tasks/docs/build.sh b/make/tasks/docs/build.sh index 5964a6913a8..6b8c74ea09d 100755 --- a/make/tasks/docs/build.sh +++ b/make/tasks/docs/build.sh @@ -5,7 +5,10 @@ set -euo pipefail SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" ROOT_DIR=$($SCRIPT_DIR/../../utilities/root_dir.sh) -swift package --package-path $ROOT_DIR --allow-writing-to-directory .build/documentation generate-documentation --target tuist --disable-indexing --output-path .build/documentation --transform-for-static-hosting -echo "/index.html /documentation/tuist" > "$ROOT_DIR/.build/documentation/_redirects" -cp $ROOT_DIR/assets/favicon.ico $ROOT_DIR/.build/documentation/favicon.ico -cp $ROOT_DIR/assets/favicon.svg $ROOT_DIR/.build/documentation/favicon.svg \ No newline at end of file +if [ ! -d $ROOT_DIR/.build/documentation ]; then + mkdir -p $ROOT_DIR/.build/documentation +fi + +swift package --package-path $ROOT_DIR/docs --allow-writing-to-directory .build/documentation generate-documentation --target tuist --disable-indexing --output-path .build/documentation --transform-for-static-hosting +cp $ROOT_DIR/assets/favicon.ico .build/documentation/favicon.ico +cp $ROOT_DIR/assets/favicon.svg .build/documentation/favicon.svg \ No newline at end of file diff --git a/make/tasks/docs/preview.sh b/make/tasks/docs/preview.sh index 653780b3128..0ede39049cc 100755 --- a/make/tasks/docs/preview.sh +++ b/make/tasks/docs/preview.sh @@ -5,4 +5,4 @@ set -euo pipefail SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" ROOT_DIR=$($SCRIPT_DIR/../../utilities/root_dir.sh) -swift package --package-path $ROOT_DIR --disable-sandbox preview-documentation --target tuist --hosting-base-path / \ No newline at end of file +swift package --package-path $ROOT_DIR/docs --disable-sandbox preview-documentation --target tuist \ No newline at end of file diff --git a/make/tasks/github/build-docc.sh b/make/tasks/github/build-docc.sh new file mode 100755 index 00000000000..db195f763b3 --- /dev/null +++ b/make/tasks/github/build-docc.sh @@ -0,0 +1,35 @@ +#!/bin/bash + +set -euo pipefail + +echo "⏳ Generating documentation for the latest release."; +mise run docs:build + +for tag in $(git -C tuist-archive tag -l --sort=v:refname | tail -n 30); +do +echo "⏳ Generating documentation for "$tag" release."; + +if [ -d ".build/documentation/$tag" ] +then + echo "✅ Documentation for "$tag" already exists."; +else + git -C tuist-archive checkout -f "$tag"; + + rm docs/Sources/tuist/ProjectDescription + ln -s ../tuist-archive/Sources/tuist/ProjectDescription docs/Sources/tuist/ProjectDescription + + swift package \ + --disable-sandbox \ + --package-path docs \ + --allow-writing-to-directory .build/documentation/"$tag" \ + generate-documentation \ + --target tuist \ + --output-path .build/documentation/"$tag" \ + --transform-for-static-hosting \ + --hosting-base-path /tuist/"$tag" \ + && echo "✅ Documentation generated for "$tag" release." \ + || echo "⚠️ Documentation skipped for "$tag"."; + cp assets/favicon.ico .build/documentation/"$tag"/favicon.ico + cp assets/favicon.svg .build/documentation/"$tag"/favicon.svg +fi; +done \ No newline at end of file diff --git a/make/tasks/tuist/acceptance-test.sh b/make/tasks/tuist/acceptance-test.sh deleted file mode 100755 index 2d61ee4dc34..00000000000 --- a/make/tasks/tuist/acceptance-test.sh +++ /dev/null @@ -1,23 +0,0 @@ -#!/bin/bash - -: ${FEATURE:=} - -set -euo pipefail - -SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -ROOT_DIR=$($SCRIPT_DIR/../../utilities/root_dir.sh) -FEATURES_DIRECTORY=$ROOT_DIR/features - -args=(--format pretty --strict-undefined) -args+=(--require "$FEATURES_DIRECTORY") - -if [ -z "$FEATURE" ]; then - args+=("$FEATURES_DIRECTORY") -else - args+=("$FEATURE") -fi - -( - cd $ROOT_DIR - bundle exec cucumber "${args[@]}" -) diff --git a/script/uninstall b/script/uninstall index ed8b0b6a085..1a138402d23 100755 --- a/script/uninstall +++ b/script/uninstall @@ -27,7 +27,7 @@ sudo_if_install_dir_not_writeable() { if [ -w $INSTALL_DIR ]; then bash -c "$command" else - bash -c "sudo \"$command\"" + bash -c "sudo $command" fi }